Stamp Test |
|
04/14/2025 |
0 |
0 |
Stopwatch |
|
04/14/2025 |
0 |
0 |
Style |
|
04/14/2025 |
0 |
0 |
Styles |
Red background green text |
04/14/2025 |
4 |
0 |
Table Dates |
Date tests ========== lastChanged 4/16/2022 4/19/1982 10/26/2024 |
04/14/2025 |
12 |
0.1 |
Tables |
Simple sparkline with inline data ================================= --- Sparkline from datatable ======================== Whats in datatable? =================== score 1 5 432 3 Show a sparkline? ================= How about a heatrix? ==================== What if we add a filter? ======================== Whats left in datatable? ======================== score 5 432 --- Visualizations from CSV ======================= name,email,phone,birthday,notes Jack Doe,john.doe@example.com,+1 (555) 123-4567,2/23/84,Daughter - Samantha. Jill Smith,jill@gmail.com,+1 (555) 123-4562,1/23/80, --- Transforms ========== name types score HTML 142 212 Markdown 192 2 Scroll 174 2 rST 142 21 name,types,score,rank HTML,142,212,1 rST,142,21,2 Markdown,192,2,3 Scroll,174,2,4 types,n,s 142,HTML,212 192,Markdown,2 174,Scroll,2 142,rST,21 types,name,score 142,HTML,212 192,Markdown,2 174,Scroll,2 142,rST,21 name HTML rST --- Impute ====== year,count 1900,10 1901,0 1902,0 1903,0 1904,0 1905,0 1906,0 1907,0 1908,0 1909,0 1910,0 1911,0 1912,0 1913,0 1914,0 1915,0 1916,0 1917,0 1918,0 1919,0 1920,0 1921,0 1922,0 1923,0 1924,0 1925,0 1926,0 1927,0 1928,0 1929,0 1930,0 1931,0 1932,0 1933,0 1934,0 1935,0 1936,0 1937,0 1938,0 1939,0 1940,0 1941,0 1942,0 1943,0 1944,0 1945,0 1946,0 1947,0 1948,0 1949,0 1950,0 1951,0 1952,0 1953,0 1954,0 1955,0 1956,0 1957,0 1958,0 1959,0 1960,0 1961,0 1962,0 1963,0 1964,0 1965,0 1966,0 1967,0 1968,0 1969,0 1970,0 1971,0 1972,0 1973,0 1974,0 1975,0 1976,0 1977,0 1978,0 1979,0 1980,0 1981,0 1982,0 1983,0 1984,0 1985,0 1986,0 1987,0 1988,0 1989,0 1990,0 1991,0 1992,0 1993,0 1994,0 1995,0 1996,0 1997,0 1998,0 1999,0 2000,122 2001,0 2002,0 2003,0 2004,0 2005,0 2006,0 2007,0 2008,0 2009,0 2010,0 2011,0 2012,0 2013,0 2014,0 2015,0 2016,0 2017,0 2018,0 2019,0 2020,23 --- Timestamps ========== folder ctime adfs 1726005360192 elmetate 1725993487049 test43 1725477868243 emailcollect 1725397672901 aadsasdfasdf2 1724954559126 folder,ctime,statement adfs,1726005360192,The name of the folder is adfs elmetate,1725993487049,The name of the folder is elmetate test43,1725477868243,The name of the folder is test43 emailcollect,1725397672901,The name of the folder is emailcollect aadsasdfasdf2,1724954559126,The name of the folder is aadsasdfasdf2 adfs Ā· elmetate Ā· test43 Ā· emailcollect Ā· aadsasdfasdf2 --- Quick Table =========== Quick JSON Table ================ [ { "name": "Jack Doe", "email": "john.doe@example.com", "phone": "+1 (555) 123-4567", "birthday": "2/23/84", "notes": "Daughter - Samantha." }, { "name": "Jill Smith", "email": "jill@gmail.com", "phone": "+1 (555) 123-4562", "birthday": "1/23/80" } ] ā --- Where tests =========== year,count 2000,122 2020,23 year,count 2020,23 --- Case insensitive ================ species virginica versicolor virginica virginica virginica setosa virginica virginica setosa setosa species virginica versicolor virginica virginica virginica setosa virginica virginica setosa setosa --- Summarize ========= name,type,incompleteCount,uniqueCount,count,sum,median,mean,min,max,mode sepal_length,number,0,8,10,57.699999999999996,5.6,5.77,4.9,7.7,5.6 sepal_width,number,0,8,10,31.599999999999998,3.2,3.1599999999999997,2.5,3.8,2.8 petal_length,number,0,8,10,39.8,4.65,3.9799999999999995,1.4,6.7,4.9 petal_width,number,0,7,10,13.699999999999996,1.75,1.3699999999999997,0.2,2.3,0.2 species,string,0,3,10,null,null,,null,null,virginica --- Empty and non empty =================== |
04/14/2025 |
622 |
3.1 |
Tabular data test |
Tabular data test ================= A test of TSV data Index Name Diameter Height Covered 5 10 15 20 25 30 35 40 45 50 55 1 Thermos 3 7.25 TRUE 149.9 147 144.6 141.2 138.2 137.1 134 132.8 131.1 129.3 127.2 2 Yeti Covered 4 3.8 TRUE 146.6 141 136.7 132.5 129 125.6 123.4 121.6 118.5 117.6 115.7 3 Yeti 4 3.8 FALSE 135.5 118.9 107.9 100.5 94.6 90.8 87.6 85.4 82.9 81.3 79.7 4 SF 4 4.5 FALSE 128.8 113.9 102.5 95.9 90.5 86.3 83.6 81.6 79.8 78.2 76.6 5 Aruba 3 3.75 FALSE 130.8 114.6 104.9 98.9 92.3 87.6 85.4 82.9 81.1 80.2 78 Putting printTable after a datatable: ===================================== name score joe 2 Dropdown sections ================= Full Table Rows LogLinear Quadratic Before After 1000 9966 1000000 36000 15000 2000 21932 4000000 53000 29000 10000 132877 100000000 258000 32000 20000 285754 400000000 1236000 52000 100000 1660964 10000000000 23122000 125000 200000 3521928 40000000000 113886000 324000 |
04/14/2025 |
208 |
1 |
Text Align |
Center Tests ============ Hello world Hello *world* This should be centered This should not be centered. This should be centered Right Tests =========== > Veni. Vidi. Vici. Julius caesar |
04/14/2025 |
26 |
0.1 |
The Scroll |
|
04/14/2025 |
0 |
0 |
Themes |
Scroll Themes ============= The themes below ship with Scroll. To use type `theme roboto` |
04/14/2025 |
13 |
0.1 |
Tufte |
Tufte ===== A header ======== A link. https://scroll.pub link - A list - Item 2 Code ==== Inline `code`. ```header A code block ``` |
04/14/2025 |
20 |
0.1 |
Txt |
Hello world |
04/14/2025 |
2 |
0 |
Video |
With loop ========= Quick video =========== |
04/14/2025 |
4 |
0 |
Settings |
|
04/14/2025 |
0 |
0 |
Sitemap |
https://scroll.pub/stampTest.html https://scroll.pub/stopwatch.html https://scroll.pub/style.html https://scroll.pub/styles.html https://scroll.pub/tableDates.html https://scroll.pub/tables.html https://scroll.pub/tabularData.html https://scroll.pub/textAlign.html https://scroll.pub/theScroll.html https://scroll.pub/themes.html https://scroll.pub/tufte.html https://scroll.pub/txt.html https://scroll.pub/video.html https://scroll.pub/settings.html https://scroll.pub/slideshowDemo.html https://scroll.pub/snippets.html https://scroll.pub/source.html https://scroll.pub/sourcemaps.html https://scroll.pub/index.html https://scroll.pub/inline.html https://scroll.pub/iris.html https://scroll.pub/javascript.html https://scroll.pub/json.html https://scroll.pub/keyboardNav.html https://scroll.pub/lists.html https://scroll.pub/macros.html https://scroll.pub/maps.html https://scroll.pub/markups.html https://scroll.pub/meta.html https://scroll.pub/modal.html https://scroll.pub/moveToFooter.html https://scroll.pub/music.html https://scroll.pub/noSnippet.html https://scroll.pub/nodejs.html https://scroll.pub/notices.html https://scroll.pub/openGraphImages.html https://scroll.pub/optionalImport.html https://scroll.pub/parsersImports.html https://scroll.pub/posts.html https://scroll.pub/prestige.html https://scroll.pub/python.html https://scroll.pub/qrcodes.html https://scroll.pub/redirectTest.html https://scroll.pub/roboto.html https://scroll.pub/script.html https://scroll.pub/scrollFormsTest.html https://scroll.pub/scrollVersion.html https://scroll.pub/buildDelimited.html https://scroll.pub/bundling.html https://scroll.pub/buttons.html https://scroll.pub/classicFormsTest.html https://scroll.pub/cloc.html https://scroll.pub/comments.html https://scroll.pub/concepts.html https://scroll.pub/contacts.html https://scroll.pub/css.html https://scroll.pub/csv.html https://scroll.pub/customMarkups.html https://scroll.pub/dark.html https://scroll.pub/dateTransforms.html https://scroll.pub/debug.html https://scroll.pub/debugging.html https://scroll.pub/defs.html https://scroll.pub/defsForm.html https://scroll.pub/dependencies.html https://scroll.pub/disk.html https://scroll.pub/dumpParsers.html https://scroll.pub/edit.html https://scroll.pub/empty.html https://scroll.pub/example.html https://scroll.pub/favicon.html https://scroll.pub/feed.html https://scroll.pub/fetchTest.html https://scroll.pub/footer.html https://scroll.pub/gazette.html https://scroll.pub/haml.html https://scroll.pub/heatrix.html https://scroll.pub/html.html https://scroll.pub/a-review-of-my-sink.html https://scroll.pub/aParser.html https://scroll.pub/about.html https://scroll.pub/atomTypes.html https://scroll.pub/authors.html https://scroll.pub/autoTitle.html https://scroll.pub/background.html https://scroll.pub/blogPost.html https://scroll.pub/top-sinks.html |
04/14/2025 |
440 |
2.2 |
Slideshow Demo |
* The Dog in the Bog * The Dog was in the Bog * The END ā |
04/14/2025 |
13 |
0.1 |
Snippets |
Regular Snippets ================ Test regular snippet printing. ============================== About the Kitchen Sink Blog =========================== 04/14/2025 https://scroll.pub/tests/about.html A review of my sink =================== 04/14/2025 https://scroll.pub/tests/a-review-of-my-sink.html Top Sinks ========= 01/11/2019 https://scroll.pub/tests/top-sinks.html --- Short Snippets ============== Test very concise snippets. =========================== About the Kitchen Sink Blog =========================== 04/14/2025 https://scroll.pub/tests/about.html A review of my sink =================== 04/14/2025 https://scroll.pub/tests/a-review-of-my-sink.html Top Sinks ========= 01/11/2019 https://scroll.pub/tests/top-sinks.html --- Full Snippets ============= Test printing full posts on one page. ===================================== Tabular data test ================= 04/14/2025 https://scroll.pub/tests/tabularData.html A review of my sink =================== 04/14/2025 https://scroll.pub/tests/a-review-of-my-sink.html Top Sinks ========= 01/11/2019 https://scroll.pub/tests/top-sinks.html --- Relative Snippets ================= Test that you can print snippets of posts in other folders. =========================================================== This is _a_ test ================ 04/14/2025 https://scroll.pub/tests/aTestPost.html Tabular data test ================= 04/14/2025 https://scroll.pub/tests/tabularData.html A review of my sink =================== 04/14/2025 https://scroll.pub/tests/a-review-of-my-sink.html Top Sinks ========= 01/11/2019 https://scroll.pub/tests/top-sinks.html |
04/14/2025 |
219 |
1.1 |
Source |
Instructions: Below are the files in a blog. I want you to read them, and then perform the following tasks, and give me your results in the format of a git patch so I can easily apply your changes on my local computer and commit them. Tasks: The Files: <?xml version="1.0" encoding="ISO-8859-1" ?> <rss version="2.0"> <channel> <title>Source</title> <link>https://scroll.pub/tests/</link> <description>Kitchen sink test site.</description> <lastBuildDate>Mon, 14 Apr 2025 13:01:00 +0000</lastBuildDate> <language>en-us</language> <item> <title>Tabular data test</title> <link>https://scroll.pub/tests/tabularData.html</link> <pubDate>Wed, 21 Apr 57255 02:42:45 +0000</pubDate> </item> <item> <title>A review of my sink</title> <link>https://scroll.pub/tests/a-review-of-my-sink.html</link> <pubDate>Wed, 21 Apr 57255 02:42:41 +0000</pubDate> </item> <item> <title>Top Sinks</title> <link>https://scroll.pub/tests/top-sinks.html</link> <pubDate>Fri, 11 Jan 2019 00:00:00 +0000</pubDate> </item> </channel> </rss> |
04/14/2025 |
174 |
0.9 |
Sourcemaps |
Source Maps tests ================= Include header.scroll: ====================== Now include footer.scroll: ========================== Built with Scroll v177.0.1 Now print source map: ===================== ``` /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:1 empty.scroll /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:1 exists true /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:1 original empty.scroll /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:1 particles 1 /home/runner/work/scroll/scroll/tests/empty.scroll:1 /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:2 # Source Maps tests /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:3 /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:4 ## Include header.scroll: /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:5 header.scroll /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:5 exists true /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:5 original header.scroll /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:5 particles 16 /home/runner/work/scroll/scroll/tests/header.scroll:1 buildHtml /home/runner/work/scroll/scroll/tests/header.scroll:2 /home/runner/work/scroll/scroll/tests/header.scroll:3 settings.scroll /home/runner/work/scroll/scroll/tests/header.scroll:3 exists true /home/runner/work/scroll/scroll/tests/header.scroll:3 original settings.scroll /home/runner/work/scroll/scroll/tests/header.scroll:3 particles 6 /home/runner/work/scroll/scroll/tests/settings.scroll:1 baseUrl https://scroll.pub/tests/ /home/runner/work/scroll/scroll/tests/settings.scroll:2 description Kitchen sink test site. /home/runner/work/scroll/scroll/tests/settings.scroll:3 editBaseUrl https://github.com/breck7/scroll/blob/main/tests/ /home/runner/work/scroll/scroll/tests/settings.scroll:4 /home/runner/work/scroll/scroll/tests/settings.scroll:5 useParserPool /home/runner/work/scroll/scroll/tests/extension.parsers /home/runner/work/scroll/scroll/tests/settings.scroll:6 /home/runner/work/scroll/scroll/tests/header.scroll:4 metaTags /home/runner/work/scroll/scroll/tests/header.scroll:5 theme gazette /home/runner/work/scroll/scroll/tests/header.scroll:6 editButton /home/runner/work/scroll/scroll/tests/header.scroll:7 homeButton /home/runner/work/scroll/scroll/tests/header.scroll:8 leftRightButtons /home/runner/work/scroll/scroll/tests/header.scroll:9 /home/runner/work/scroll/scroll/tests/header.scroll:10 /home/runner/work/scroll/scroll/tests/header.scroll:11 ## Now include footer.scroll: /home/runner/work/scroll/scroll/tests/header.scroll:12 footer.scroll /home/runner/work/scroll/scroll/tests/header.scroll:12 exists true /home/runner/work/scroll/scroll/tests/header.scroll:12 original footer.scroll /home/runner/work/scroll/scroll/tests/header.scroll:12 particles 7 /home/runner/work/scroll/scroll/tests/footer.scroll:1 clearStack /home/runner/work/scroll/scroll/tests/footer.scroll:2 center /home/runner/work/scroll/scroll/tests/footer.scroll:3 emailButton feedback@scroll.pub /home/runner/work/scroll/scroll/tests/footer.scroll:4 downloadButton download.html /home/runner/work/scroll/scroll/tests/footer.scroll:5 editButton /home/runner/work/scroll/scroll/tests/footer.scroll:6 scrollVersionLink /home/runner/work/scroll/scroll/tests/footer.scroll:7 /home/runner/work/scroll/scroll/tests/header.scroll:13 /home/runner/work/scroll/scroll/tests/header.scroll:14 ## Now print source map: /home/runner/work/scroll/scroll/tests/header.scroll:15 // Test this using assertHtmlMatches rather than assertHtmlIncludes because a simple string search of the source would just match the string in the source. /home/runner/work/scroll/scroll/tests/header.scroll:16 debugSourceMap /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:6 assertHtmlMatches settings.scroll:\d extension.parsers /home/runner/work/scroll/scroll/tests/sourcemaps.scroll:7 ``` |
04/14/2025 |
587 |
2.9 |
Inline |
Hello world |
04/14/2025 |
2 |
0 |
Iris |
sepal_length,sepal_width,petal_length,petal_width,species 6.2,3.4,5.4,2.3,virginica 6.2,2.8,4.8,1.8,virginica 4.9,2.5,4.5,1.7,virginica 5,3.4,1.5,0.2,setosa 7.7,3.8,6.7,2.2,virginica 5.3,3.7,1.5,0.2,setosa 5.6,2.7,4.2,1.3,versicolor 6.1,3,4.9,1.8,virginica 5.6,2.8,4.9,2,virginica 5.1,3.5,1.4,0.2,setosa |
04/14/2025 |
92 |
0.5 |
Javascript |
The background should be green |
04/14/2025 |
5 |
0 |
Json |
window.package.description says: { "name": "scroll-cli", "version": "177.0.1", "description": "A language for scientists of all ages. A curated collection of tools for refining and sharing thoughts.", "main": "scroll.js", "engines": { "node": ">=18.0" }, "scripts": { "beta": "cp ~/sdk/products/*.js node_modules/scrollsdk/products", "build": "node ./scroll.js list | node ./scroll.js build", "coverage": "tap tests/scroll.test.js", "format": "scroll list | scroll format", "open": "open index.html", "debug": "node --inspect tests/scroll.test.js;", "test": "node tests/scroll.test.js", "up": "npm install scrollsdk@latest" }, "bin": { "scroll": "./scroll.js", "clone": "./clone.js" }, "files": [ "scroll.js", "ScrollSetCLI.js", "simpleCli.js", "parsers/", "external/", "microlangs/", "stripeBuyButton.scroll", "header.scroll" ], "repository": { "type": "git", "url": "git+https://github.com/breck7/scroll.git" }, "author": "Breck Yunits", "prettier": { "parser": "babel", "useTabs": false, "tabWidth": 2, "semi": false, "printWidth": 240, "trailingComma": "none", "arrowParens": "avoid", "quoteProps": "as-needed" }, "homepage": "https://scroll.pub", "dependencies": { "d3": "^6.7.0", "dayjs": "^1.11.7", "image-size": "^1.0.2", "lodash": "^4.17.21", "minimist": "^1.2.5", "scroll-cli": "^170.2.0", "scrollsdk": "^107.0.1" }, "devDependencies": { "prettier": "^2.8.8", "tap": "^18.7.2" } } { "name": "scroll-cli", "version": "177.0.1", "description": "A language for scientists of all ages. A curated collection of tools for refining and sharing thoughts.", "main": "scroll.js", "engines": { "node": ">=18.0" }, "scripts": { "beta": "cp ~/sdk/products/*.js node_modules/scrollsdk/products", "build": "node ./scroll.js list | node ./scroll.js build", "coverage": "tap tests/scroll.test.js", "format": "scroll list | scroll format", "open": "open index.html", "debug": "node --inspect tests/scroll.test.js;", "test": "node tests/scroll.test.js", "up": "npm install scrollsdk@latest" }, "bin": { "scroll": "./scroll.js", "clone": "./clone.js" }, "files": [ "scroll.js", "ScrollSetCLI.js", "simpleCli.js", "parsers/", "external/", "microlangs/", "stripeBuyButton.scroll", "header.scroll" ], "repository": { "type": "git", "url": "git+https://github.com/breck7/scroll.git" }, "author": "Breck Yunits", "prettier": { "parser": "babel", "useTabs": false, "tabWidth": 2, "semi": false, "printWidth": 240, "trailingComma": "none", "arrowParens": "avoid", "quoteProps": "as-needed" }, "homepage": "https://scroll.pub", "dependencies": { "d3": "^6.7.0", "dayjs": "^1.11.7", "image-size": "^1.0.2", "lodash": "^4.17.21", "minimist": "^1.2.5", "scroll-cli": "^170.2.0", "scrollsdk": "^107.0.1" }, "devDependencies": { "prettier": "^2.8.8", "tap": "^18.7.2" } } |
04/14/2025 |
364 |
1.8 |
Keyboard Nav |
|
04/14/2025 |
0 |
0 |
Lists |
- This is a test - Of nested - Indentation - Level 4 |
04/14/2025 |
9 |
0 |
Macros |
I went to south of the border and end up in Turkey. I went to south of the border and end up in Mexico. Multiline macros ================ Two roads diverged and I I took the one less travelled by. |
04/14/2025 |
38 |
0.2 |
Maps |
Geolocate user ============== Custom ====== |
04/14/2025 |
3 |
0 |
Markups |
Hello world This is italicized Make this code Strikethrough this whole line. hi *world* .a.-b- |
04/14/2025 |
16 |
0.1 |
Meta |
date,year,title,permalink,authors,tags,wordCount,minutes 01/06/2025,2025,Data Science with Scroll,dataScience.html,Breck Yunits,All,715,3.6 10/28/2024,2024,Scroll in 50 Questions,scrollInQuestions.html,Breck Yunits,All,1082,5.4 10/01/2024,2024,Web Forms for Geniuses,forms.html,Breck Yunits,All,369,1.8 09/02/2024,2024,The Successor to RSS is Git Clone,gitOverRss.html,Breck Yunits,All,255,1.3 08/30/2024,2024,A New Way to Program,teddyTalk.html,Breck Yunits,All,425,2.1 08/09/2024,2024,Datatables: a microlang for data science,tables.html,Breck Yunits,All,479,2.4 08/02/2024,2024,Build Your Own Counters With 3 Words,counters.html,Breck Yunits,All,1029,5.1 07/28/2024,2024,Heatrix: Heat Maps + Matrix Visualizations,heatrix.html,Breck Yunits,All,470,2.4 07/16/2024,2024,Contacts: A Microlang for Managing Contacts,contacts.html,Breck Yunits,All,374,1.9 07/12/2024,2024,How I Designed Helpful 404's and Stopped Worrying about Broken Links,helpful404s.html,Breck Yunits,All,547,2.7 07/09/2024,2024,Convert CSVs to ScrollSets,csvToScrollSet.html,Breck Yunits,All,100,0.5 07/01/2024,2024,Changes: A Microlang for Change Logs,changes.html,Breck Yunits,All,431,2.2 06/24/2024,2024,Write scatterplot to get a scatterplot,plot.html,Breck Yunits,All,342,1.7 06/23/2024,2024,Stamp: a microlang for project templates,stamp.html,Breck Yunits,All,306,1.5 04/29/2024,2024,Scroll builds text files,textFiles.html,Breck Yunits,All,175,0.9 04/21/2024,2024,ScrollSets: source code for CSVs,scrollsets.html,Breck Yunits,All ScrollSets,1382,6.9 05/08/2023,2023,Introducing Program Links,programLinks.html,Breck Yunits,All,522,2.6 04/13/2023,2023,Counterpoints: A new tool for thought in the Scroll Language,counterpoints.html,Breck Yunits,All,391,2 10/02/2022,2022,Introducing the Blink Tag,introducingBlink.html,Breck Yunits,All,176,0.9 08/23/2021,2021,"The Power of Indented Heredocs: Markdown, Textile, and BBCode in one file with no escaping",indented-heredocs.html,Breck Yunits,All,455,2.3 |
04/14/2025 |
372 |
1.9 |
Modal |
Open modal. /#m1 modal This should be in a modal ========================= Open modal 2. /#m2 modal This is modal 2. ================ |
04/14/2025 |
19 |
0.1 |
Move To Footer |
This is my body. This should be above the footer. This is my footer. It is moved to end of file before compilation. The End. |
04/14/2025 |
25 |
0.1 |
Music |
With loop ========= Quick audio =========== Quick audio with query string ============================= Do not match quick audio ======================== sipOfCoffee.m4a is an audio file |
04/14/2025 |
20 |
0.1 |
No Snippet |
|
04/14/2025 |
0 |
0 |
Nodejs |
The score is 3 A header ======== This should be a subheader ========================== |
04/14/2025 |
11 |
0.1 |
Notices |
|
04/14/2025 |
0 |
0 |
Open Graph Images |
Not this one ============ [Image Omitted] https://scroll.pub/index.html undefined The image below should be the og image ====================================== [Image Omitted] Not this one ============ [Image Omitted] A caption https://scroll.pub/blog/scrollsets.html undefined |
04/14/2025 |
35 |
0.2 |
Optional Import |
Pass 1 - codeAtStart ======== undefined Pass 2 - fusedCode ======== [Unchanged] Pass 3 - codeAfterMacros ======== buildHtml imported /home/runner/work/scroll/scroll/tests/settings.scroll exists true original settings.scroll particles 6 baseUrl https://scroll.pub/tests/ description Kitchen sink test site. editBaseUrl https://github.com/breck7/scroll/blob/main/tests/ useParserPool /home/runner/work/scroll/scroll/tests/extension.parsers // todo: the below optional line should be in the importer code. // optional imported /home/runner/work/scroll/scroll/tests/env.scroll exists false original env.scroll particles 0 debugSourceStack |
04/14/2025 |
85 |
0.4 |
Parsers Imports |
|
04/14/2025 |
0 |
0 |
Posts |
--- This is _a_ testĀ·Tabular data testĀ·A review of my sinkĀ·Top Sinks --- 1..2..3..4 --- <a href="subfolder/aTestPost.html">This is _a_ test</a><br><a href="tabularData.html">Tabular data test</a><br><a href="a-review-of-my-sink.html">A review of my sink</a><br><a href="top-sinks.html">Top Sinks</a> --- <span style="background-color: #2a2d34ff; width: 30px; height: 30px; display: inline-block;"> </span></><span style="background-color: #009ddcff; width: 30px; height: 30px; display: inline-block;"> </span></><span style="background-color: #f26430ff; width: 30px; height: 30px; display: inline-block;"> </span></><span style="background-color: #6761a8ff; width: 30px; height: 30px; display: inline-block;"> </span></><span style="background-color: #009b72ff; width: 30px; height: 30px; display: inline-block;"> </span> <span style="background-color: #2a2d34ff; width: 30px; height: 30px; display: inline-block;"> </span></><span style="background-color: #009ddcff; width: 30px; height: 30px; display: inline-block;"> </span> |
04/14/2025 |
159 |
0.8 |
Prestige |
Julius Caesar ============= Prestige ======== * * ā |
04/14/2025 |
3 |
0 |
Python |
2 + 2 is: 4 |
04/14/2025 |
4 |
0 |
Qrcodes |
|
04/14/2025 |
0 |
0 |
Redirect Test |
|
04/14/2025 |
0 |
0 |
Roboto |
Roboto ====== A header ======== A link. https://scroll.pub link - A list - Item 2 Code ==== Inline `code`. ```header A code block ``` |
04/14/2025 |
20 |
0.1 |
Script |
|
04/14/2025 |
0 |
0 |
Scroll Forms Test |
ScrollSet Form Maker ==================== Past: to generate a classic form write "classicForm" ==================================================== Future: to generate a Scroll Form write "scrollForm" ==================================================== |
04/14/2025 |
19 |
0.1 |
Scroll Version |
Built with Scroll v177.0.1 |
04/14/2025 |
6 |
0 |
Build Delimited |
sepal_length,sepal_width,petal_length,petal_width,species 6.1,3,4.9,1.8,virginica 5.6,2.8,4.9,2,virginica 6.2,2.8,4.8,1.8,virginica 7.7,3.8,6.7,2.2,virginica 6.2,3.4,5.4,2.3,virginica 4.9,2.5,4.5,1.7,virginica |
04/14/2025 |
57 |
0.3 |
Bundling |
|
04/14/2025 |
0 |
0 |
Buttons |
A link https://scroll.pub A link A Post https://hub.scroll.pub/echo.htm A Post post hello world |
04/14/2025 |
20 |
0.1 |
Classic Forms Test |
|
04/14/2025 |
0 |
0 |
Cloc |
files,language,blank,comment,code 27,JavaScript,7127,8132,56674 113,Parsers,461,0,8051 147,Scroll,2135,0,6015 15,CSS,264,101,3401 2,SVG,0,0,226 2,JSON,0,0,112 4,YAML,6,3,65 2,Markdown,17,0,37 1,Text,0,0,8 1,HTML,0,0,5 1,Python,0,0,1 315,SUM,10010,8236,74595 |
04/14/2025 |
65 |
0.3 |
Comments |
|
04/14/2025 |
0 |
0 |
Concepts |
Concepts ======== name,email,phone,birthday,notes Jack Doe,john.doe@example.com,+1 (555) 123-4567,2/23/84,Daughter - Samantha. Jill Smith,jill@gmail.com,+1 (555) 123-4562,1/23/80, This is a page for Jack Doe =========================== His phone is +1 (555) 123-4567 |
04/14/2025 |
47 |
0.2 |
My Contacts |
My Contacts =========== name,email,phone,birthday,notes Jack Doe,john.doe@example.com,+1 (555) 123-4567,2/23/84,Daughter - Samantha. Jill Smith,jill@gmail.com,+1 (555) 123-4562,1/23/80, |
04/14/2025 |
34 |
0.2 |
Css |
The background should be green |
04/14/2025 |
5 |
0 |
Csv |
|
04/14/2025 |
0 |
0 |
Custom Markups |
Custom Inline Markups Test ========================== *This* should not be bold. ~~~This should be a marquee.~~~ And _this_ should be underlined. A :link:. And $this text$ should be green. |
04/14/2025 |
27 |
0.1 |
Dark |
Dark ==== A header ======== A link. https://scroll.pub link - A list - Item 2 Code ==== Inline `code`. ```header A code block ``` |
04/14/2025 |
20 |
0.1 |
Date Transforms |
version,releaseDate,bugFixes,newFeatures,breakingChanges,year,month,dayOfMonth,day,monthName,dayName 145.3.0,10/24/2024,0,1,0,2024,10,24,4,October,Thursday 58.5.0,3/2/2023,0,1,0,2023,3,2,4,March,Thursday 148.3.1,11/12/2024,1,0,0,2024,11,12,2,November,Tuesday 28.0.0,8/26/2022,0,1,12,2022,8,26,5,August,Friday 100.0.0,7/02/2024,0,2,1,2024,7,2,2,July,Tuesday 89.2.0,5/16/2024,0,1,0,2024,5,16,4,May,Thursday 54.0.0,1/19/2023,1,2,2,2023,1,19,4,January,Thursday 107.1.0,7/14/2024,0,1,0,2024,7,14,0,July,Sunday 84.5.1,5/07/2024,1,0,0,2024,5,7,2,May,Tuesday 145.1.1,10/23/2024,1,0,0,2024,10,23,3,October,Wednesday |
04/14/2025 |
161 |
0.8 |
Debug |
Hello world /foo.html Hello world ================================== inspectAbove |
04/14/2025 |
7 |
0 |
Debug tools tests |
Debug tools tests ================= The sum of 1+1 is 2 Pass 1 - codeAtStart ======== undefined Pass 2 - fusedCode ======== [Unchanged] Pass 3 - codeAfterMacros ======== buildHtml replace TITLE Debug tools tests replaceJs SUM 1+1 noFormat title Debug tools tests theme gazette printTitle The sum of 1+1 is 2 debugSourceStack imported /home/runner/work/scroll/scroll/tests/footer.scroll exists true original footer.scroll particles 7 clearStack center emailButton feedback@scroll.pub downloadButton download.html editButton scrollVersionLink Built with Scroll v177.0.1 |
04/14/2025 |
80 |
0.4 |
Defs |
|
04/14/2025 |
0 |
0 |
Defs Form |
This example shows the use of Scroll Defs to create forms even faster than writing Parsers. |
04/14/2025 |
16 |
0.1 |
Dependencies |
This file requires these files: =============================== file /home/runner/work/scroll/scroll/tests/header.scroll /home/runner/work/scroll/scroll/tests/settings.scroll /home/runner/work/scroll/scroll/tests/extension.parsers /home/runner/work/scroll/scroll/microlangs/changes.parsers ../particles.png ../style.css contacts.csv /home/runner/work/scroll/scroll/tests/footer.scroll [Image Omitted] Built with Scroll v177.0.1 |
04/14/2025 |
60 |
0.3 |
Disk |
[{"name":".clocLangs.txt","type":"file","size":215,"lastModified":"2025-04-14T13:01:00.291Z"},{"name":".codeMirror.css","type":"file","size":9413,"lastModified":"2025-04-14T13:01:00.747Z"},{"name":".constants.js","type":"file","size":214465,"lastModified":"2025-04-14T13:01:00.752Z"},{"name":".dark.css","type":"file","size":551,"lastModified":"2025-04-14T13:01:00.713Z"},{"name":".datatables.css","type":"file","size":57644,"lastModified":"2025-04-14T13:01:00.272Z"},{"name":".datatables.js","type":"file","size":697329,"lastModified":"2025-04-14T13:01:00.277Z"},{"name":".dayjs.min.js","type":"file","size":14556,"lastModified":"2025-04-14T13:01:00.273Z"},{"name":".gazette.css","type":"file","size":628,"lastModified":"2025-04-14T13:01:00.256Z"},{"name":".jquery-3.7.1.min.js","type":"file","size":87533,"lastModified":"2025-04-14T13:01:00.272Z"},{"name":".scroll.css","type":"file","size":8921,"lastModified":"2025-04-14T13:01:00.256Z"},{"name":".scrollLibs.js","type":"file","size":961754,"lastModified":"2025-04-14T13:01:00.751Z"},{"name":".tableSearch.js","type":"file","size":7018,"lastModified":"2025-04-14T13:01:00.278Z"},{"name":"a-review-of-my-sink.html","type":"file","size":7009,"lastModified":"2025-04-14T13:01:00.262Z"},{"name":"a-review-of-my-sink.scroll","type":"file","size":656,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"aParser.html","type":"file","size":405,"lastModified":"2025-04-14T13:01:00.264Z"},{"name":"aParser.scroll","type":"file","size":460,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"about.html","type":"file","size":5151,"lastModified":"2025-04-14T13:01:00.266Z"},{"name":"about.scroll","type":"file","size":451,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"atomTypes.html","type":"file","size":90,"lastModified":"2025-04-14T13:01:00.266Z"},{"name":"atomTypes.scroll","type":"file","size":439,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"authors.scroll","type":"file","size":106,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"autoTitle.html","type":"file","size":391,"lastModified":"2025-04-14T13:01:00.267Z"},{"name":"autoTitle.scroll","type":"file","size":149,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"background.html","type":"file","size":161,"lastModified":"2025-04-14T13:01:00.268Z"},{"name":"background.scroll","type":"file","size":41,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"blogPost.html","type":"file","size":8731,"lastModified":"2025-04-14T13:01:00.270Z"},{"name":"blogPost.scroll","type":"file","size":71,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"browserstack.node.js","type":"file","size":2521,"lastModified":"2025-04-14T13:00:38.561Z"},{"name":"buildDelimited.csv","type":"file","size":209,"lastModified":"2025-04-14T13:01:00.196Z"},{"name":"buildDelimited.html","type":"file","size":1180,"lastModified":"2025-04-14T13:01:00.278Z"},{"name":"buildDelimited.json","type":"file","size":802,"lastModified":"2025-04-14T13:01:00.197Z"},{"name":"buildDelimited.scroll","type":"file","size":148,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"buildDelimited.tsv","type":"file","size":209,"lastModified":"2025-04-14T13:01:00.197Z"},{"name":"buildDelimited.txt","type":"file","size":210,"lastModified":"2025-04-14T13:01:00.271Z"},{"name":"bundling.css","type":"file","size":3067,"lastModified":"2025-04-14T13:01:00.279Z"},{"name":"bundling.js","type":"file","size":7993,"lastModified":"2025-04-14T13:01:00.279Z"},{"name":"bundling.scroll","type":"file","size":212,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"buttons.html","type":"file","size":750,"lastModified":"2025-04-14T13:01:00.280Z"},{"name":"buttons.scroll","type":"file","size":145,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"classicFormsTest.html","type":"file","size":1285,"lastModified":"2025-04-14T13:01:00.291Z"},{"name":"classicFormsTest.scroll","type":"file","size":240,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"cloc.html","type":"file","size":1650,"lastModified":"2025-04-14T13:01:00.707Z"},{"name":"cloc.scroll","type":"file","size":68,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"clocLangs.txt","type":"file","size":215,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"comments.scroll","type":"file","size":25,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"concepts.html","type":"file","size":1306,"lastModified":"2025-04-14T13:01:00.710Z"},{"name":"concepts.scroll","type":"file","size":373,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"contacts.csv","type":"file","size":161,"lastModified":"2025-04-14T13:01:00.198Z"},{"name":"contacts.html","type":"file","size":1257,"lastModified":"2025-04-14T13:01:00.711Z"},{"name":"contacts.json","type":"file","size":295,"lastModified":"2025-04-14T13:01:00.198Z"},{"name":"contacts.scroll","type":"file","size":554,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"contacts.tsv","type":"file","size":161,"lastModified":"2025-04-14T13:01:00.198Z"},{"name":"content.scroll","type":"file","size":174,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"css.css","type":"file","size":29,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"css.html","type":"file","size":187,"lastModified":"2025-04-14T13:01:00.712Z"},{"name":"css.scroll","type":"file","size":50,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"csv.csv","type":"file","size":19,"lastModified":"2025-04-14T13:01:00.207Z"},{"name":"csv.scroll","type":"file","size":169,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"customMarkups.html","type":"file","size":819,"lastModified":"2025-04-14T13:01:00.713Z"},{"name":"customMarkups.scroll","type":"file","size":377,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"dark.html","type":"file","size":1338,"lastModified":"2025-04-14T13:01:00.714Z"},{"name":"dark.scroll","type":"file","size":38,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"dateTransforms.html","type":"file","size":1954,"lastModified":"2025-04-14T13:01:00.745Z"},{"name":"dateTransforms.scroll","type":"file","size":457,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"debug.html","type":"file","size":222,"lastModified":"2025-04-14T13:01:00.745Z"},{"name":"debug.scroll","type":"file","size":65,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"debugging.html","type":"file","size":4761,"lastModified":"2025-04-14T13:01:00.747Z"},{"name":"debugging.scroll","type":"file","size":164,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"defs.parsers","type":"file","size":814,"lastModified":"2025-04-14T13:01:00.208Z"},{"name":"defs.scroll","type":"file","size":304,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"defsForm.html","type":"file","size":3677,"lastModified":"2025-04-14T13:01:00.759Z"},{"name":"defsForm.scroll","type":"file","size":166,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"dependencies.html","type":"file","size":9526,"lastModified":"2025-04-14T13:01:00.761Z"},{"name":"dependencies.scroll","type":"file","size":441,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"disk.html","type":"file","size":15022,"lastModified":"2025-04-14T13:01:00.765Z"},{"name":"disk.scroll","type":"file","size":75,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"dumpParsers.scroll","type":"file","size":243,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"dumpParsers.txt","type":"file","size":266706,"lastModified":"2025-04-14T13:01:00.215Z"},{"name":"edit.html","type":"file","size":1840,"lastModified":"2025-04-14T13:01:00.766Z"},{"name":"edit.scroll","type":"file","size":72,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"empty.scroll","type":"file","size":0,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"example.scroll","type":"file","size":279,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"extension.parsers","type":"file","size":164,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"favicon.html","type":"file","size":836,"lastModified":"2025-04-14T13:01:00.766Z"},{"name":"favicon.scroll","type":"file","size":45,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"feed.rss","type":"file","size":764,"lastModified":"2025-04-14T13:01:00.768Z"},{"name":"feed.scroll","type":"file","size":69,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"fetchTest.scroll","type":"file","size":129,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"footer.scroll","type":"file","size":108,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"gazette.html","type":"file","size":1347,"lastModified":"2025-04-14T13:01:00.769Z"},{"name":"gazette.scroll","type":"file","size":44,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"haml.html","type":"file","size":469,"lastModified":"2025-04-14T13:01:00.770Z"},{"name":"haml.scroll","type":"file","size":264,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"haml.txt","type":"file","size":70,"lastModified":"2025-04-14T13:01:00.770Z"},{"name":"header.scroll","type":"file","size":100,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"heatrix.html","type":"file","size":10668,"lastModified":"2025-04-14T13:01:00.772Z"},{"name":"heatrix.scroll","type":"file","size":535,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"html.html","type":"file","size":364,"lastModified":"2025-04-14T13:01:00.773Z"},{"name":"html.scroll","type":"file","size":233,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"htmlInclude.html","type":"file","size":65,"lastModified":"2025-04-14T13:00:38.562Z"},{"name":"index.scroll","type":"file","size":89,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"inline.scroll","type":"file","size":95,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"iris.scroll","type":"file","size":66,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"javascript.js","type":"file","size":46,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"javascript.scroll","type":"file","size":56,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"json.scroll","type":"file","size":217,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"keyboardNav.scroll","type":"file","size":51,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"lists.scroll","type":"file","size":84,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"macros.scroll","type":"file","size":330,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"maps.scroll","type":"file","size":1436,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"markups.scroll","type":"file","size":351,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"meta.csv","type":"file","size":1975,"lastModified":"2025-04-14T13:01:00.234Z"},{"name":"meta.scroll","type":"file","size":115,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"modal.scroll","type":"file","size":185,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"moveToFooter.scroll","type":"file","size":188,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"music.scroll","type":"file","size":297,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"noSnippet.scroll","type":"file","size":0,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"nodejs.scroll","type":"file","size":244,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"notices.scroll","type":"file","size":19,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"openGraphImages.scroll","type":"file","size":303,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"optionalImport.scroll","type":"file","size":131,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"parsersImports.scroll","type":"file","size":171,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"postTemplate.scroll","type":"file","size":92,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"posts.scroll","type":"file","size":570,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"prestige.scroll","type":"file","size":115,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"python.py","type":"file","size":12,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"python.scroll","type":"file","size":39,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"qrcodes.scroll","type":"file","size":53,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"redirectTest.scroll","type":"file","size":22,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"roboto.scroll","type":"file","size":42,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"script.scroll","type":"file","size":134,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"scroll.test.js","type":"file","size":14454,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"scrollFormsTest.scroll","type":"file","size":955,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"scrollVersion.scroll","type":"file","size":43,"lastModified":"2025-04-14T13:00:38.563Z"},{"name":"settings.scroll","type":"file","size":151,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"sipOfCoffee.m4a","type":"file","size":45809,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"sitemap.scroll","type":"file","size":63,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"slideshowDemo.scroll","type":"file","size":127,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"snippets.scroll","type":"file","size":434,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"source.scroll","type":"file","size":400,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"sourcemaps.scroll","type":"file","size":371,"lastModified":"2025-04-14T13:00:38.564Z"},{"name":"spirit.mp4","type":"file","size":153185,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"stampTest.scroll","type":"file","size":193,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"stopwatch.scroll","type":"file","size":88,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"style.scroll","type":"file","size":133,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"styles.scroll","type":"file","size":82,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"subfolder","type":"directory","size":4096,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"tableDates.scroll","type":"file","size":141,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"tables.scroll","type":"file","size":1796,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"tabularData.scroll","type":"file","size":1062,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"testOutput","type":"directory","size":4096,"lastModified":"2025-04-14T13:01:00.255Z"},{"name":"textAlign.scroll","type":"file","size":311,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"theScroll.scroll","type":"file","size":27,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"themes.scroll","type":"file","size":165,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"top-sinks.scroll","type":"file","size":740,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"tufte.scroll","type":"file","size":40,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"txt.scroll","type":"file","size":138,"lastModified":"2025-04-14T13:00:38.565Z"},{"name":"video.scroll","type":"file","size":123,"lastModified":"2025-04-14T13:00:38.565Z"}] |
04/14/2025 |
2226 |
11.1 |
Dump Parsers |
``` // Related work: CSS is great for text selector ideas: https://www.w3schools.com/cssref/css_selectors.asp Roff has a lot of related markup ideas: https://www.systutorials.com/docs/linux/man/7-groff_man/ // Link atom types // File system atom types // HTML atom types // todo: copy the external library to folder so it works offline? // todo: these should not be parsers in stdlib, but should be made through componsition of more reusable parsers in a dataflow style. // String Measures // URL Parsers // Required ID measure which denotes a concept // Numeric Measures // Enum Measures // Boolean Measures // Date and time measures // todo Add imports parsers, along with source maps, so we can correctly support parsers split across multiple files, and better enable parsers from compositions of reusable bits? // todo Do error checking for if you have a firstatomAtomType, atoms, and/or catchAllAtomType with same name. // todo Add enumOption root level type? // todo compile atoms. add javascript property. move getRunTimeEnumOptions to atoms. // Atom Parsers // Date atom types // Line Parsers // Enums // https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap#text // A joint effort from: // - Breck Yunits https://github.com/breck7 // - Guillaume Papin https://github.com/Sarcasm // Origin: https://github.com/breck7/scrollsdk/issues/120 assertionKindAtom enum includes excludes equals paint constant columnNameAtom paint constant newColumnNameAtom description Name a derived column. paint variable constantAtom paint constant percentAtom paint constant.numeric.float extends stringAtom // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex countAtom extends integerAtom yearAtom extends integerAtom parseTimeCommandAtom extends cueAtom description Give parse time command atoms their own color. paint constant.character.escape delimiterAtom description String to use as a delimiter. paint string bulletPointAtom description Any token used as a bullet point such as "-" or "1." or ">" paint keyword comparisonAtom enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith oneOf paint constant personNameAtom extends stringAtom urlAtom paint constant.language absoluteUrlAtom paint constant.language regex (ftp|https?)://.+ emailAddressAtom extends stringAtom paint constant.language permalinkAtom paint string description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance. filePathAtom paint constant.language outputFileExtensionAtom extends fileExtensionAtom enum html txt css js json pdf tsv csv parsers rss tagOrUrlAtom description An HTML tag or a url. paint constant.language htmlAttributesAtom paint constant htmlMethodAtom paint constant enum get post htmlTagAtom paint constant.language enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code htmlTypeAtom extends stringAtom paint constant.language enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range description HTML input type attribute values. classNameAtom paint constant htmlIdAtom paint constant fontFamilyAtom enum Arial Helvetica Verdana Georgia Impact Tahoma Slim paint constant javascriptAnyAtom extends codeAtom htmlAnyAtom extends codeAtom colorAtom extends codeAtom namedColorAtom extends colorAtom enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen mimeTypeAtom extends stringAtom paint constant.language description File MIME types (e.g. 'image/*', 'application/pdf') enum image/* image/jpeg image/png image/gif image/webp image/svg+xml application/pdf text/plain text/html text/css text/javascript text/csv application/json application/xml application/zip application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio/* audio/mpeg audio/wav audio/ogg video/* video/mp4 video/webm video/ogg buildCommandAtom extends cueAtom description Give build command atoms their own color. paint constant cssAnyAtom extends codeAtom cssValueAtom extends cssAnyAtom cssLengthAtom extends codeAtom cssSelectorAtom paint keyword.control examples body h1 // todo add html tags, css and ids selector regexes, etc cssVendorPrefixCueAtom description Properties like -moz-column-fill paint variable.function extends cueAtom cssPropertyNameAtom paint variable.function // todo Where are these coming from? Can we add a url link enum align-content align-items align-self all animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function backface-visibility background background-attachment background-blend-mode background-clip background-color background-image background-origin background-position background-repeat background-size border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-shadow box-sizing break-inside caption-side clear clip color column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset cursor direction display empty-atoms fill filter flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float font @font-face font-family font-size font-size-adjust font-stretch font-style font-variant font-weight hanging-punctuation height hyphens justify-content @keyframes left letter-spacing line-height list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top max-height max-width @media min-height min-width nav-down nav-index nav-left nav-right nav-up opacity order outline outline-color outline-offset outline-style outline-width overflow overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page-break-after page-break-before page-break-inside perspective perspective-origin position quotes resize right tab-size table-layout text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-style text-indent text-justify text-overflow text-shadow text-transform top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi vertical-align visibility white-space width atom-break atom-spacing atom-wrap z-index overscroll-behavior-x user-select -ms-touch-action -webkit-user-select -webkit-touch-callout -moz-user-select touch-action -ms-user-select -khtml-user-select gap grid-auto-flow grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end grid-row-gap grid-row-start grid-template-columns grid-template-rows justify-items justify-self measureTypeAtom extends stringAtom paint constant.language description Type of the measure enum string url float int bool date reductionTypeAtom enum sum mean max min concat first paint keyword inlineMarkupNameAtom description Options to turn on some inline markups. enum bold italics code katex none tileOptionAtom enum default light measureNameAtom extends cueAtom // A regex for column names for max compatibility with a broad range of data science tools: regex [a-zA-Z][a-zA-Z0-9]* abstractConstantAtom paint entity.name.tag javascriptSafeAlphaNumericIdentifierAtom regex [a-zA-Z0-9_]+ reservedAtoms enum extends function static if while export return class for default require var let const new anyAtom baseParsersAtom description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error. // todo Remove? enum blobParser errorParser paint variable.parameter enumAtom paint constant.language booleanAtom enum true false extends enumAtom atomParserAtom enum prefix postfix omnifix paint constant.numeric atomPropertyNameAtom paint variable.parameter atomTypeIdAtom examples integerAtom cueAtom someCustomAtom extends javascriptSafeAlphaNumericIdentifierAtom enumFromAtomTypes atomTypeIdAtom paint storage constantIdentifierAtom examples someId myVar // todo Extend javascriptSafeAlphaNumericIdentifier regex [a-zA-Z]\w+ paint constant.other description A atom that can be assigned to the parser in the target language. constructorFilePathAtom enumOptionAtom // todo Add an enumOption top level type, so we can add data to an enum option such as a description. paint string atomExampleAtom description Holds an example for a atom with a wide range of options. paint string extraAtom paint invalid fileExtensionAtom examples js txt doc exe regex [a-zA-Z0-9]+ paint string numberAtom paint constant.numeric floatAtom extends numberAtom regex \-?[0-9]*\.?[0-9]* paint constant.numeric.float integerAtom regex \-?[0-9]+ extends numberAtom paint constant.numeric.integer cueAtom description A atom that indicates a certain parser to use. paint keyword javascriptCodeAtom lowercaseAtom regex [a-z]+ parserIdAtom examples commentParser addParser description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it. paint variable.parameter extends javascriptSafeAlphaNumericIdentifierAtom enumFromAtomTypes parserIdAtom cueAtom paint constant.language regexAtom paint string.regexp reservedAtomAtom description A atom that a atom cannot contain. paint string paintTypeAtom enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter paint string scriptUrlAtom semanticVersionAtom examples 1.0.0 2.2.1 regex [0-9]+\.[0-9]+\.[0-9]+ paint constant.numeric dateAtom paint string stringAtom paint string atomAtom paint string description A non-empty single atom string. regex .+ exampleAnyAtom examples lorem ipsem // todo Eventually we want to be able to parse correctly the examples. paint comment extends stringAtom blankAtom commentAtom paint comment codeAtom paint comment columnNameOrColorAtom extends columnNameAtom metaCommandAtom extends cueAtom description Give meta command atoms their own color. paint constant.numeric // Obviously this is not numeric. But I like the green color for now. We need a better design to replace this "paint" concept https://github.com/breck7/scrollsdk/issues/186 vegaDataSetAtom paint constant.numeric enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv tagAtom extends permalinkAtom tagWithOptionalFolderAtom description A group name optionally combined with a folder path. Only used when referencing tags, not in posts. extends stringAtom scrollThemeAtom enum roboto gazette dark tufte prestige paint constant abstractScrollParser atoms cueAtom javascript buildHtml() { return "" } buildHtmlSnippet(buildSettings) { return this.buildHtml(buildSettings) } buildTxt() { return "" } getHtmlRequirements(buildSettings) { const {requireOnce} = this if (!requireOnce) return "" const set = buildSettings?.alreadyRequired || this.root.alreadyRequired if (set.has(requireOnce)) return "" set.add(requireOnce) return requireOnce + "\n\n" } abstractAftertextParser description Text followed by markup commands. extends abstractScrollParser inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser string defaultClassName scrollParagraph string tag p javascript get markupInserts() { const { originalTextPostLinkify } = this return this.filter(particle => particle.isMarkup) .map(particle => particle.getInserts(originalTextPostLinkify)) .filter(i => i) .flat() } buildHtmlSnippet(buildSettings) { if (this.has("noSnippet")) return "" return super.buildHtmlSnippet(buildSettings) } get originalText() { return this.content ?? "" } get originalTextPostLinkify() { const { originalText } = this const shouldLinkify = this.get("linkify") === "false" || originalText.includes("<a ") ? false : true return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText } replaceNotes(originalText) { // Skip the replacements if there are no footnotes or the text has none. if (!this.root.footnotes.length || !originalText.includes("^")) return originalText this.root.footnotes.forEach((note, index) => { const needle = note.cue const {linkBack} = note if (originalText.includes(needle)) originalText = originalText.replace(new RegExp("\\" + needle + "\\b"), `<a href="#${note.htmlId}" class="scrollNoteLink" id="${linkBack}"><sup>${note.label}</sup></a>`) }) return originalText } get text() { const { originalTextPostLinkify, markupInserts } = this let adjustment = 0 let newText = originalTextPostLinkify markupInserts.sort((a, b) => { if (a.index !== b.index) return a.index - b.index // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks. if (b.index === b.endIndex) // unless the endIndex is the same as index return a.endIndex - b.endIndex return b.endIndex - a.endIndex }) markupInserts.forEach(insertion => { insertion.index += adjustment const consumeStartCharacters = insertion.consumeStartCharacters ?? 0 const consumeEndCharacters = insertion.consumeEndCharacters ?? 0 newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters) adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters }) return newText } get className() { if (this.get("class")) return this.get("class") const classLine = this.getParticle("addClass") if (classLine && classLine.applyToParentElement) return classLine.content return this.defaultClassName } get isHidden() { return this.has("hidden") } buildHtml(buildSettings) { if (this.isHidden) return "" this.buildSettings = buildSettings const { className, styles } = this const classAttr = className ? `class="${this.className}"` : "" const selfClose = this.isSelfClosing ? " /" : "" const tag = this.get("tag") || this.tag if (tag === "none") // Allow no tag for aftertext in tables return this.text const id = this.has("id") ? "" : `id="${this.htmlId}" ` // always add an html id return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}` } get htmlContents() { return this.text } get closingTag() { if (this.isSelfClosing) return "" const tag = this.get("tag") || this.tag return `</${tag}>` } get htmlAttributes() { const attrs = this.filter(particle => particle.isAttribute) return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(" ") + " " : "" } get inlineStyles() { // Todo: cleanup const style = this.getParticle("style") const fontFamily = this.getParticle("font") const color = this.getParticle("color") const right = this.getParticle("right") if (!style && !fontFamily && !color && !right) return "" return `${style?.content};${fontFamily?.css};${color?.css};${right?.css}` } get styles() { const {inlineStyles} = this return inlineStyles ? ` style="${inlineStyles}"` : "" } get htmlId() { return this.get("id") || "particle" + this.index } scrollParagraphParser // todo Perhaps rewrite this from scratch and move out of aftertext. extends abstractAftertextParser catchAllAtomType stringAtom description A paragraph. boolean suggestInAutocomplete false cueFromId javascript buildHtml(buildSettings) { if (this.isHidden) return "" // Hacky, I know. const newLine = this.has("inlineMarkupsOn") ? undefined : this.appendLine("inlineMarkupsOn") const compiled = super.buildHtml(buildSettings) if (newLine) newLine.destroy() return compiled } buildTxt() { const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join("\n") const dateline = this.getParticle("dateline") return (dateline ? dateline.day + "\n\n" : "") + (this.originalText || "") + (subparticles ? "\n " + subparticles.replace(/\n/g, "\n ") : "") } authorsParser popularity 0.007379 // multiple authors delimited by " and " boolean isPopular true extends scrollParagraphParser description Set author(s) name(s). example authors Breck Yunits https://breckyunits.com Breck Yunits // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser atoms metaCommandAtom boolean isTopMatter true boolean isSetterParser true boolean noSnippet true string defaultClassName printAuthorsParser javascript buildHtml() { return "" } buildHtmlForPrint() { // hacky. todo: cleanup const originalContent = this.content this.setContent(`by ${originalContent}`) const html = super.buildHtml() this.setContent(originalContent) return html } buildTxtForPrint() { return 'by ' + super.buildTxt() } buildTxt() { return "" } blinkParser description Just for fun. extends scrollParagraphParser example blink Carpe diem! cue blink javascript buildHtml() { return `<span class="scrollBlink">${super.buildHtml()}</span> <script>setInterval(()=>{ Array.from(document.getElementsByClassName("scrollBlink")).forEach(el => el.style.visibility = el.style.visibility === "hidden" ? "visible" : "hidden") }, 500)</script>` } scrollButtonParser extends scrollParagraphParser cueFromId description A button. postParser description Post a particle. example scrollButton Click me string defaultClassName scrollButton string tag button javascript get htmlAttributes() { const link = this.getFromParser("scrollLinkParser") const post = this.getParticle("post") if (post) { const method = "post" const action = link?.link || "" const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString() return ` onclick="fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;" ` } return super.htmlAttributes + (link ? `onclick="window.location='${link.link}'"` : "") } getFromParser(parserId) { return this.find(particle => particle.doesExtend(parserId)) } catchAllParagraphParser popularity 0.115562 description A paragraph. extends scrollParagraphParser boolean suggestInAutocomplete false boolean isPopular true boolean isArticleContent true atoms stringAtom javascript getErrors() { const errors = super.getErrors() || [] return this.parent.has("testStrict") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors } get originalText() { return this.getLine() || "" } scrollCenterParser popularity 0.006415 cue center description A centered section. extends scrollParagraphParser example center This paragraph is centered. javascript buildHtml() { this.parent.sectionStack.push("</center>") return `<center>${super.buildHtml()}` } buildTxt() { return this.content } abstractIndentableParagraphParser extends scrollParagraphParser inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser javascript get htmlContents() { return this.text + this.filter(particle => particle.buildHtml).map(particle => particle.buildHtml()) .join("\n") .trim() } buildTxt() { return this.getAtom(0) + " " + super.buildTxt() } checklistTodoParser popularity 0.000193 extends abstractIndentableParagraphParser example [] Get milk description A task todo. cue [] string checked javascript get text() { return `<div style="text-indent:${(this.getIndentLevel() - 1) * 20}px;"><input type="checkbox" ${this.checked} id="${this.id}"><label for="${this.id}">` + super.text + `</label></div>` } get id() { return this.get("id") || "item" + this._getUid() } checklistDoneParser popularity 0.000072 extends checklistTodoParser description A completed task. string checked checked cue [x] example [x] get milk abstractHtmlElementParser description HTML tag. extends abstractIndentableParagraphParser javascript defaultClassName = "" get tag() { return this.cue} buildTxt() { return "" } htmlSectionParser extends abstractHtmlElementParser cue section example section # Hello world p This is a test section # Deeper htmlHeaderParser extends abstractHtmlElementParser cue header example header # Title htmlFooterParser extends abstractHtmlElementParser cue footer example footer p Made with love htmlAsideParser extends abstractHtmlElementParser cue aside example aside h1 Some notes htmlArticleParser extends abstractHtmlElementParser cue article example article h1 My article htmlMainParser extends abstractHtmlElementParser cue main example main # Title htmlNavParser extends abstractHtmlElementParser cue nav example nav a Home href / htmlPreParser extends abstractHtmlElementParser cue pre example pre htmlUlParser extends abstractHtmlElementParser cue ul example ul li A list htmlOlParser extends abstractHtmlElementParser cue ol example ol li A list htmlLiParser extends abstractHtmlElementParser cue li example ol li A list htmlImgParser extends abstractHtmlElementParser cue img boolean isSelfClosing true example img foo.png htmlAParser extends abstractHtmlElementParser cue a example a Home href / htmlFormParser extends abstractHtmlElementParser cue form example form input htmlInputParser extends abstractHtmlElementParser cue input boolean isSelfClosing true example input type text placeholder Enter your name htmlSelectParser extends abstractHtmlElementParser cue select example select option Value 1 option Value 2 htmlOptionParser extends abstractHtmlElementParser cue option example select option Choose an option option First Option htmlTextareaParser extends abstractHtmlElementParser cue textarea example textarea placeholder Enter your message rows 4 htmlButtonParser extends abstractHtmlElementParser cue button example button Submit type submit htmlLabelParser extends abstractHtmlElementParser cue label example label Name for username htmlSpanParser extends abstractHtmlElementParser cue span example span Hello htmlCanvasParser extends abstractHtmlElementParser cue canvas example canvas htmlIframeParser extends abstractHtmlElementParser cue iframe example iframe h1LiteralParser extends abstractHtmlElementParser cue h1 example main h1 Title h2LiteralParser extends abstractHtmlElementParser cue h2 example main h2 Title h3LiteralParser extends abstractHtmlElementParser cue h3 example main h3 Title h4LiteralParser extends abstractHtmlElementParser cue h4 example main h4 Title htmlKbdParser extends abstractHtmlElementParser cue kbd example main kbd Ctrl+ā htmlMetaTagParser extends abstractHtmlElementParser cue meta boolean isSelfClosing true example meta name description content A great page about meta tags htmlDivParser extends abstractHtmlElementParser cue div example div # Hello world div # Deeper htmlStrongParser extends abstractHtmlElementParser cue strong example strong Important text htmlEmParser extends abstractHtmlElementParser cue em example em Emphasized text htmlBlockquoteParser extends abstractHtmlElementParser cue blockquote example blockquote p A wise quote htmlTableParser extends abstractHtmlElementParser cue table example table tr th Header 1 th Header 2 tr td Cell 1 td Cell 2 htmlTrParser extends abstractHtmlElementParser cue tr example tr td Cell 1 htmlTdParser extends abstractHtmlElementParser cue td example td Cell content htmlThParser extends abstractHtmlElementParser cue th example th Header content htmlTheadParser extends abstractHtmlElementParser cue thead example thead tr th Header 1 htmlTbodyParser extends abstractHtmlElementParser cue tbody example tbody tr td Cell 1 listAftertextParser popularity 0.014325 extends abstractIndentableParagraphParser example - I had a _new_ thought. description A list item. cue - string tag li string listType ul string defaultClassName string attributes javascript buildHtml() { const {index, parent} = this const particleClass = this.constructor const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass) const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass) const { listType } = this return (isStartOfList ? `<${listType} ${this.attributes}>` : "") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : "") } abstractCustomListItemParser extends listAftertextParser javascript get requireOnce() { return `<style>\n.${this.constructor.name} li::marker {content: "${this.cue} ";}\n</style>` } get attributes() { return `class="${this.constructor.name}"` } orderedListAftertextParser popularity 0.004485 extends listAftertextParser description A list item. example 1. Hello world pattern ^\d+\. string listType ol javascript get attributes() { return ` start="${this.getAtom(0)}"`} pParagraphParser popularity 0.001881 cue p extends abstractIndentableParagraphParser description A paragraph. boolean isArticleContent true example p I had a _new_ idea. quickQuoteParser popularity 0.000482 cue > example > The only thing we have to fear is fear itself. - FDR boolean isPopular true extends abstractIndentableParagraphParser description A quote. string defaultClassName scrollQuote string tag blockquote scrollCounterParser description Visualize the speed of something. extends scrollParagraphParser cue counter example counter 4.5 Babies Born atoms cueAtom numberAtom javascript buildHtml() { const line = this.getLine() const atoms = line.split(" ") atoms.shift() // drop the counter atom const perSecond = parseFloat(atoms.shift()) // get number const increment = perSecond/10 const id = this._getUid() this.setLine(`* <span id="counter${id}" title="0">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(" ")) const html = super.buildHtml() this.setLine(line) return html } expanderParser popularity 0.000072 cueFromId description An collapsible HTML details tag. extends scrollParagraphParser string tag summary string defaultClassName example expander Knock Knock Who's there? javascript buildHtml() { this.parent.sectionStack.push("</details>") return `<details>${super.buildHtml()}` } buildTxt() { return this.content } footnoteDefinitionParser popularity 0.001953 description A footnote. Can also be used as section notes. extends scrollParagraphParser boolean isFootnote true pattern ^\^.+$ // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again. inScope quickLinkParser labelParser description If you want to show a custom label for a footnote. Default label is the note definition index. cueFromId atoms cueAtom catchAllAtomType stringAtom javascript get htmlId() { return `note${this.noteDefinitionIndex}` } get label() { // In the future we could allow common practices like author name return this.get("label") || `[${this.noteDefinitionIndex}]` } get linkBack() { return `noteUsage${this.noteDefinitionIndex}` } get text() { return `<a class="scrollFootNoteUsageLink" href="#noteUsage${this.noteDefinitionIndex}">${this.label}</a> ${super.text}` } get noteDefinitionIndex() { return this.parent.footnotes.indexOf(this) + 1 } buildTxt() { return this.getAtom(0) + ": " + super.buildTxt() } abstractHeaderParser extends scrollParagraphParser example # Hello world boolean isHeader true javascript buildHtml(buildSettings) { if (this.isHidden) return "" if (this.parent.sectionStack) this.parent.sectionStack.push("</div>") return `<div class="scrollSection">` + super.buildHtml(buildSettings) } buildTxt() { const line = super.buildTxt() return line + "\n" + "=".repeat(line.length) } h1Parser popularity 0.017918 description An html h1 tag. extends abstractHeaderParser example # Hello world boolean isArticleContent true cue # boolean isPopular true string tag h1 h2Parser popularity 0.005257 description An html h2 tag. extends abstractHeaderParser example ## Hello world boolean isArticleContent true cue ## boolean isPopular true string tag h2 h3Parser popularity 0.001085 description An html h3 tag. extends abstractHeaderParser example ### Hello world boolean isArticleContent true cue ### string tag h3 h4Parser popularity 0.000289 description An html h4 tag. example #### Hello world extends abstractHeaderParser cue #### string tag h4 scrollQuestionParser popularity 0.004244 description A question. extends h4Parser cue ? example ? Why is the sky blue? string defaultClassName scrollQuestion h5Parser description An html h5 tag. extends abstractHeaderParser example ##### Hello world cue ##### string tag h5 printTitleParser popularity 0.007572 description Print title. extends abstractHeaderParser boolean isPopular true example title Eureka printTitle cueFromId string defaultClassName printTitleParser string tag h1 javascript buildHtml(buildSettings) { // Hacky, I know. const {content} = this if (content === undefined) this.setContent(this.root.title) const { permalink } = this.root if (!permalink) { this.setContent(content) // Restore it as it was. return super.buildHtml(buildSettings) } const newLine = this.appendLine(`link ${permalink}`) const compiled = super.buildHtml(buildSettings) newLine.destroy() this.setContent(content) // Restore it as it was. return compiled } get originalText() { return this.content ?? this.root.title ?? "" } captionAftertextParser popularity 0.003207 description An image caption. cue caption example map.png caption A map. extends scrollParagraphParser boolean isPopular true abstractMediaParser extends scrollParagraphParser inScope scrollMediaLoopParser scrollAutoplayParser int atomIndex 1 javascript buildTxt() { return "" } get filename() { return this.getAtom(this.atomIndex) } getAsHtmlAttribute(attr) { if (!this.has(attr)) return "" const value = this.get(attr) return value ? `${attr}="${value}"` : attr } getAsHtmlAttributes(list) { return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(" ") } buildHtml() { return `<${this.tag} src="${this.filename}" controls ${this.getAsHtmlAttributes("width height loop autoplay".split(" "))}></${this.tag}>` } scrollMusicParser popularity 0.000024 extends abstractMediaParser cue music description Play sound files. example music sipOfCoffee.m4a javascript buildHtml() { return `<audio controls ${this.getAsHtmlAttributes("loop autoplay".split(" "))}><source src="${this.filename}" type="audio/mpeg"></audio>` } quickSoundParser popularity 0.000024 extends scrollMusicParser atoms urlAtom pattern ^[^\s]+\.(mp3|wav|ogg|aac|m4a|flac)(?:\?[^\s]*)?$ int atomIndex 0 example sipOfCoffee.m4a scrollVideoParser popularity 0.000024 extends abstractMediaParser cue video example video spirit.mp4 description Play video files. widthParser cueFromId atoms cueAtom integerAtom heightParser cueFromId atoms cueAtom integerAtom string tag video quickVideoParser popularity 0.000024 extends scrollVideoParser example spirit.mp4 atoms urlAtom pattern ^[^\s]+\.(mp4|webm|avi|mov)(?:\?[^\s]*)?$ int atomIndex 0 quickParagraphParser popularity 0.001881 cue * extends scrollParagraphParser description A paragraph. boolean isArticleContent true example * I had a _new_ idea. scrollStopwatchParser description A stopwatch. extends scrollParagraphParser cue stopwatch example stopwatch atoms cueAtom catchAllAtomType numberAtom javascript buildHtml() { const line = this.getLine() const id = this._getUid() this.setLine(`* <span class="scrollStopwatchParser" id="stopwatch${id}">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `) const html = super.buildHtml() this.setLine(line) return html } thinColumnsParser popularity 0.003690 extends abstractAftertextParser cueFromId catchAllAtomType integerAtom description Thin columns. int columnWidth 35 int columnGap 20 boolean noSnippet true javascript buildHtml() { const {columnWidth, columnGap, maxColumns} = this const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack. if (this.singleColumn) this.parent.sectionStack.push("</div>") // Single columns are self-closing after section break. return stackContents + `<div class="scrollColumns" style="column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;">` } get maxColumns() { return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10) } wideColumnsParser popularity 0.000386 extends thinColumnsParser description Wide columns. int columnWidth 90 wideColumnParser popularity 0.003376 extends wideColumnsParser description A wide column section. boolean singleColumn true mediumColumnsParser popularity 0.003376 extends thinColumnsParser description Medium width columns. int columnWidth 65 mediumColumnParser popularity 0.003376 extends mediumColumnsParser description A medium column section. boolean singleColumn true thinColumnParser popularity 0.003376 extends thinColumnsParser description A thin column section. boolean singleColumn true endColumnsParser popularity 0.007789 extends abstractAftertextParser cueFromId description End columns. boolean noSnippet true javascript buildHtml() { return "</div>" } scrollContainerParser popularity 0.000096 cue container description A centered HTML div. example container 600px catchAllAtomType cssLengthAtom extends abstractAftertextParser boolean isHtml true string tag div string defaultClassName scrollContainerParser string closingTag string text boolean noSnippet true javascript get maxWidth() { return this.atoms[1] || "1200px"} get inlineStyles() { return `width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;` + super.inlineStyles} buildHtml() { this.parent.bodyStack.push("</div>") return super.buildHtml() } debugSourceStackParser // useful for debugging description Print compilation steps. extends abstractAftertextParser cueFromId example printOriginalSource javascript get sources() { const {file} = this.root const passNames = ["codeAtStart", "fusedCode", "codeAfterMacros"] let lastCode = "" return passNames.map(name => { let code = name === "codeAfterMacros" ? this.root.toString() : file[name] if (lastCode === code) code = "[Unchanged]" lastCode = file[name] return { name, code }}) } buildHtml() { return `<code class="scrollCodeBlock">${this.buildTxt().replace(/\</g, "<")}</code>` } buildTxt() { return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\n========\n${pass.code}`).join("\n\n\n") } abstractDinkusParser extends abstractAftertextParser boolean isDinkus true string defaultClass abstractDinkusParser javascript buildHtml() { return `<div class="${this.defaultClass}"><span>${this.dinkus}</span></div>` } buildTxt() { return this.dinkus } get dinkus() { return this.content || this.getLine() } horizontalRuleParser popularity 0.000362 cue --- description A horizontal rule. extends abstractDinkusParser example --- javascript buildHtml() { return `<hr>` } scrollDinkusParser popularity 0.010828 cue *** description A dinkus. Breaks section. boolean isPopular true example *** extends abstractDinkusParser string dinkus * customDinkusParser cue dinkus description A custom dinkus. atoms cueAtom stringAtom extends abstractDinkusParser endOfPostDinkusParser popularity 0.005740 extends abstractDinkusParser description End of post dinkus. boolean isPopular true cue **** example **** string dinkus ā abstractIconButtonParser extends abstractAftertextParser cueFromId boolean noSnippet true javascript buildHtml() { return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href="${this.link}" class="doNotPrint abstractIconButtonParser" style="${this.style}">${this.svg}</a>` } downloadButtonParser popularity 0.006294 description Link to download/WWS page. extends abstractIconButtonParser catchAllAtomType urlAtom string style position:relative; string svg <svg fill="#000000" xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve"><path d="M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> javascript get link() { return this.content } editButtonParser popularity 0.013963 description Print badge top right. extends abstractIconButtonParser catchAllAtomType urlAtom // SVG from https://github.com/32pixelsCo/zest-icons string svg <svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z"/></svg> javascript get link() { return this.content || this.root.editUrl || "" } get style() { return this.parent.findParticles("editButton")[0] === this ? "right:2rem;": "position:relative;" } emailButtonParser popularity 0.006294 description Email button. extends abstractIconButtonParser catchAllAtomType emailAddressAtom // todo: should just be "optionalAtomType" string style position:relative; string svg <svg viewBox="3 5 24 20" width="24" height="20" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1, 0, 0, 1, 0, -289.0625)"><path style="opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z " transform="translate(0,289.0625)" id="rect4592"/></g></svg> javascript get link() { const email = this.content || this.parent.get("email") return email ? `mailto:${email}` : "" } homeButtonParser popularity 0.006391 description Home button. extends abstractIconButtonParser catchAllAtomType urlAtom string style left:2rem; string svg <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z"/></svg> javascript get link() { return this.content || this.get("link") || "index.html" } theScrollButtonParser popularity 0.006294 description WWS button. extends abstractIconButtonParser string style position:relative; string svg <svg xmlns="http://www.w3.org/2000/svg" direction="ltr" width="231.52568231268793" height="249.33156169975996" viewBox="297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996" stroke-linecap="round" stroke-linejoin="round" data-color-mode="dark" class="tl-container tl-theme__force-sRGB tl-theme__dark" ><defs/><g transform="matrix(1, 0, 0, 1, 395.9682, 1413.3618)" opacity="1"><g transform="scale(1)"><path d="M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z" stroke-linecap="round"/></g></g></svg> string link https://wws.scroll.pub abstractTextLinkParser extends abstractAftertextParser cueFromId boolean noSnippet true javascript buildTxt() { return this.text } buildHtml() { return `<div class="abstractTextLinkParser"><a href="${this.link}">${this.text}</a></div>` } editLinkParser popularity 0.001206 extends abstractTextLinkParser description Print "Edit" link. string text Edit javascript get link() { return this.root.editUrl || "" } scrollVersionLinkParser popularity 0.006294 extends abstractTextLinkParser string link https://scroll.pub description Print Scroll version. javascript get text() { return `Built with Scroll v${this.root.scrollVersion}` } classicFormParser cue classicForm popularity 0.006391 description Generate input form for ScrollSet. extends abstractAftertextParser atoms cueAtom catchAllAtomType stringAtom string script <script> sendFormViaEmail = form => { const mailto = new URL("mailto:") const params = [] const { value, title } = form.querySelector('button[type="submit"]') params.push(`subject=${encodeURIComponent(value)}`) params.push(`to=${encodeURIComponent(title)}`) const oneTextarea = form.querySelector('textarea[title="oneTextarea"]') const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join("\\n") params.push(`body=${encodeURIComponent(body)}`) mailto.search = params.join("&") window.open(mailto.href, '_blank') } </script> string style <style> .scrollFormParser { font-family: "Gill Sans", "Bitstream Vera Sans", sans-serif; } .scrollFormParser input , .scrollFormParser textarea{ padding: 10px; margin-bottom: 10px; width: 100%; box-sizing: border-box; } .scrollFormParser label { display: block; margin-bottom: 5px; } </style> string footer javascript get inputs() { return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => { const {Name, Question, IsRequired, Type} = measure const type = Type || "text" const placeholder = Question const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1) // ${index ? "" : "autofocus"} let tag = "" if (Type === "textarea") tag = `<textarea placeholder="${placeholder}" id="${Name}" name="${Name}" ${IsRequired ? "required" : ""}></textarea>` else tag = `<input placeholder="${placeholder}" type="${type}" id="${Name}" name="${Name}" ${IsRequired ? "required" : ""}>` return `<div><label for="${Name}" title="${IsRequired ? "Required" : ""}">${ucFirst}${IsRequired ? "*" : ""}:</label>${tag}</div>` }).join("\n") } buildHtml() { const {isEmail, formDestination, callToAction, subject} = this return `${this.script}${this.style}<form ${isEmail ? "onsubmit='sendFormViaEmail(this); return false;'" : ` method='post' action='${formDestination}'`} class="scrollFormParser">${this.inputs}<button value="${subject}" title="${formDestination}" class="scrollButton" type="submit">${callToAction}</button>${this.footer}</form>` } get callToAction() { return (this.isEmail ? "Submit via email" : (this.subject || "Post")) } get isEmail() { return this.formDestination.includes("@") } get formDestination() { return this.getAtom(1) || "" } get subject() { return this.getAtomsFrom(2)?.join(" ") || "" } scrollFormParser extends classicFormParser cue scrollForm placeholderParser atoms cueAtom baseParser blobParser cueFromId single valueParser atoms cueAtom baseParser blobParser cueFromId single nameParser description Name for the post submission. atoms cueAtom stringAtom cueFromId single description Generate a Scroll Form. string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js string requireOnce <link rel="stylesheet" href=".codeMirror.css"> <script src=".scrollLibs.js"></script> <script src=".constants.js"></script> string footer javascript get placeholder() { return this.getParticle("placeholder")?.subparticlesToString() || "" } get value() { return this.getParticle("value")?.subparticlesToString() || "" } get name() { return this.get("name") || "particles" } get parsersBundle() { const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm const clone = this.root.clone() const parsers = clone.filter(line => parserRegex.test(line.getLine())) return "\n" + parsers.map(particle => { particle.prependLine("boolean suggestInAutocomplete true") return particle.toString() }).join("\n") } get inputs() { const Name = this.name return `<textarea title="oneTextarea" rows="${Math.min(this.root.measures.length * 2, 30)}" placeholder="${this.placeholder}" id="${Name}" name="${Name}"></textarea> <script id="${Name}Parsers" type="text/plain">${this.parsersBundle}</script> <script>{ const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm let {width, height} = document.getElementById('${Name}').getBoundingClientRect(); const sp = new Particle(AppConstants.parsers) sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => { part.appendLine("boolean suggestInAutocomplete false") }) const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById("${Name}Parsers").textContent).compileAndReturnRootParser() codeMirrorInstance = new ParsersCodeMirrorMode("custom", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById("${Name}"), { lineWrapping: false, lineNumbers: false }) codeMirrorInstance.setSize(width, height); codeMirrorInstance.setValue(\`${this.value}\`); }</script>` } buildHtml(buildSettings) { return this.getHtmlRequirements(buildSettings) + super.buildHtml() } loremIpsumParser extends abstractAftertextParser cueFromId description Generate dummy text. catchAllAtomType integerAtom string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. javascript get originalText() { return this.placeholder.repeat(this.howMany) } get howMany() { return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1 } nickelbackIpsumParser extends loremIpsumParser string placeholder And one day, Iāll be at the door. And lose your wings to fall in love? To the bottom of every bottle. Iām on the ledge of the eighteenth story. Why must the blind always lead the blind? scrollModalParser description A modal dialog overlay. extends abstractAftertextParser boolean isHtml true cue modal string requireOnce <style> .scrollModal { display: none; position: fixed; background: transparent; padding: 20px; left: 3rem; top: 3rem; right: 3rem; bottom: 3rem; z-index: 1000; font-family: "IBM Plex Mono", monospace; /* Frosted glass effect */ background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; overflow: hidden; } .scrollModal .closeButton { position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; border: none; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.8); border-radius: 4px; cursor: pointer; font-family: "IBM Plex Mono", monospace; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .scrollModal .closeButton:hover { background: rgba(255, 255, 255, 0.2); color: white; } </style> <script>{ window.closeModal = modal => { modal.style.display = 'none'; history.pushState("", document.title, window.location.pathname + window.location.search); }; window.openModal = (id, event) => { if (event) event.modalJustOpened = true; document.querySelector("#" + id).style.display = 'block'; }; window.addEventListener('hashchange', () => { const modalId = location.hash.slice(1); window.openModal(modalId); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.scrollModal').forEach(closeModal); }); // Check hash on initial load document.addEventListener("DOMContentLoaded", e => { if (location.hash) window.openModal(location.hash.slice(1)); }); // Close on outside click document.addEventListener('click', e => { if (e.modalJustOpened) return; const modals = document.querySelectorAll('.scrollModal'); modals.forEach(modal => { if (modal.style.display === 'block' && !modal.contains(e.target)) { closeModal(modal); } }); }); }</script> javascript buildHtml(buildSettings) { this.parent.sectionStack.push("</div>") return this.getHtmlRequirements(buildSettings) + `<div class="scrollModal" id="${this.htmlId}"><button class="closeButton" onclick="closeModal(this.parentElement)">Ć</button>` } printSnippetsParser popularity 0.000338 // todo: why are we extending AT here and not loops? Is it for class/id etc? extends abstractAftertextParser cueFromId atoms cueAtom catchAllAtomType tagWithOptionalFolderAtom description Prints snippets matching tag(s). example printSnippets index javascript makeSnippet(scrollProgram, buildSettings) { const {endSnippetIndex} = scrollProgram if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink const joinChar = "\n" const html = scrollProgram .map((subparticle, index) => (index >= endSnippetIndex || subparticle.noSnippet ? "" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : (subparticle.buildHtml ? subparticle.buildHtml(buildSettings) : "") )) .filter(i => i) .join(joinChar) .trim() + `<a class="scrollContinueReadingLink" href="${linkRelativeToCompileTarget}">Continue reading...</a>` return html } get files() { const thisFile = this.parent.file const files = this.root.getFilesByTags(this.content, this.has("limit") ? parseInt(this.get("limit")) : undefined).filter(file => file.file !== thisFile) // allow sortBy lastCommit Time if (this.get("sortBy") === "commitTime") { return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse() } return files } buildHtml() { const alreadyRequired = this.root.alreadyRequired const snippets = this.files.map(file => { const buildSettings = {relativePath: file.relativePath, alreadyRequired } return `<div class="scrollSnippetContainer">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>` }).join("\n\n") return `<div class="scrollColumns" style="column-width:35ch;">${snippets}</div>` } buildTxt() { return this.files.map(file => { const {scrollProgram} = file.file const {title, date, absoluteLink} = scrollProgram const ruler = "=".repeat(title.length) // Note: I tried to print the description here but the description generating code needs work. return `${title}\n${ruler}\n${date}\n${absoluteLink}` }).join("\n\n") } scrollNavParser popularity 0.000048 extends printSnippetsParser cue fileNav description Titles and links in group(s). joinParser boolean allowTrailingWhitespace true cueFromId atoms cueAtom catchAllAtomType stringAtom javascript buildHtml() { return `<nav class="scrollNavParser">` + this.root.getFilesByTags(this.content).map(file => { const { linkTitle, permalink } = file.file.scrollProgram return `<a href="${permalink}">${linkTitle}</a>` }).join(this.get("join") || " ") + `</nav>` } printFullSnippetsParser popularity 0.000048 extends printSnippetsParser cueFromId description Print full pages in group(s). javascript makeSnippet(scrollProgram, buildSettings) { return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml } printShortSnippetsParser popularity 0.000048 extends printSnippetsParser cueFromId description Titles and descriptions in group(s). javascript makeSnippet(scrollProgram, buildSettings) { const { title, permalink, description, dateObject } = scrollProgram return `<div><a href="${permalink}">${title}</a><div>${description}...</div><div class="subdued" style="text-align:right;">${dateObject.format(`MMMM D, YYYY`)}</div></div>` } printRelatedParser popularity 0.001182 description Print links to related posts. extends printSnippetsParser cueFromId javascript buildHtml() { const alreadyRequired = this.root.alreadyRequired const list = this.files.map(fileWrapper => { const {relativePath, file} = fileWrapper const {title, permalink, year} = file.scrollProgram return `- ${title}${year ? " (" + year + ")" : ""}\n link ${relativePath + permalink}` }).join("\n") const items = this.parent.concat(list) const html = items.map(item => item.buildHtml()).join("\n") items.forEach(item => item.destroy()) return html } scrollNoticesParser extends abstractAftertextParser description Display messages in URL query parameters. cue notices javascript buildHtml() { const id = this.htmlId return `<div id="${id}" class="scrollNoticesParser" style="display:none;"></div><script>(function(){ const params = new URLSearchParams(window.location.search) if (!params.size) return document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => { if (key === "error") return '<div style="color:red">Error: ' + value + '</div>' if (key === "success") return '<div style="color:green">Success: ' + value + '</div>' return '<div>' + key + ': ' + value + '</div>' }).join("") || "No query parameters found" document.getElementById('${id}').style.display = "block" })()</script>` } abstractAssertionParser description Test above particle's output. extends abstractScrollParser string bindTo previous catchAllAtomType codeAtom cueFromId javascript get particleToTest() { // If the previous particle is also an assertion particle, use the one before that. return this.previous.particleToTest ? this.previous.particleToTest : this.previous } get outputFormat() { return "html"} get actual() { let {outputFormat} = this outputFormat = outputFormat.substr(0, 1).toUpperCase() + outputFormat.substr(1) const methodName = `build${outputFormat}` return this.particleToTest[methodName]() } getErrors() { const {actual, expected} = this const errors = super.getErrors() if (this.areEqual(actual, expected)) return errors return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`)) } get expected() { return this.length ? this.subparticlesToString() : (this.content ? this.content : "") } catchAllParser htmlLineParser assertParser extends abstractAssertionParser atoms cueAtom outputFileExtensionAtom assertionKindAtom catchAllAtomType stringAtom example Hello assert txt includes Hello javascript get outputFormat() { return this.atoms[1] } get expected() { return this.length ? this.subparticlesToString() : this.getAtomsFrom(3).join(" ") } get kind() { return this.atoms[2] } areEqual(actual, expected) { const operator = this.kind if (operator === "includes") return actual.includes(expected) if (operator === "excludes") return !actual.includes(expected) if (operator === "equals") return actual === expected } assertHtmlEqualsParser extends abstractAssertionParser string kind equal example <b></b> assertHtmlEquals <b></b> javascript areEqual(actual, expected) { return actual === expected } assertBuildIncludesParser extends abstractAssertionParser string kind include example buildCsv assertBuildIncludes virginica javascript areEqual(actual, expected) { return actual.includes(expected) } get actual() { return this.particleToTest.buildOutput()} assertBuildEndsWithParser extends assertBuildIncludesParser string kind endsWith example buildCsv assertBuildEndsWith virginica javascript areEqual(actual, expected) { // Note that this does do a trim on both. return actual.trim().endsWith(expected.trim()) } assertHtmlIncludesParser extends abstractAssertionParser string kind include javascript areEqual(actual, expected) { return actual.includes(expected) } assertHtmlMatchesParser extends abstractAssertionParser string kind matches javascript areEqual(actual, expected) { return actual.match(new RegExp(expected)) } assertHtmlExcludesParser extends abstractAssertionParser string kind exclude javascript areEqual(actual, expected) { return !actual.includes(expected) } assertSilenceBelowErrorsParser description Silence errors in the below particle in automated tests. extends abstractScrollParser string bindTo next cueFromId javascript getErrors() { if (!this.next) return [] this.next.silenceErrors = true this.next.topDownArray.forEach(part => part.silenceErrors = true) return [] } abstractPrintMetaParser extends abstractScrollParser cueFromId printAuthorsParser popularity 0.001664 description Prints author(s) byline. boolean isPopular true extends abstractPrintMetaParser // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom catchAllAtomType stringAtom example authors Breck Yunits https://breckyunits.com printAuthors javascript buildHtml() { return this.parent.getParticle("authors")?.buildHtmlForPrint() } buildTxt() { return this.parent.getParticle("authors")?.buildTxtForPrint() } printDateParser popularity 0.000434 extends abstractPrintMetaParser // If not present computes the date from the file's ctime. description Print published date. boolean isPopular true example date 1/11/2019 printDate javascript buildHtml() { return `<div class="printDateParser">${this.day}</div>` } get day() { let day = this.content || this.root.date if (!day) return "" return this.root.dayjs(day).format(`MMMM D, YYYY`) } buildTxt() { return this.day } printFormatLinksParser description Prints links to other formats. extends abstractPrintMetaParser example printFormatLinks javascript buildHtml() { const permalink = this.root.permalink.replace(".html", "") // hacky const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\nlink ${permalink}.html HTML\nlink ${permalink}.txt TXT\nstyle text-align:center;`) const html = particle.buildHtml() particle.destroy() return html } buildTxt() { const permalink = this.root.permalink.replace(".html", "") return `HTML | TXT\n link ${permalink}.html HTML\n link ${permalink}.txt TXT` } abstractBuildCommandParser extends abstractScrollParser cueFromId atoms buildCommandAtom catchAllAtomType filePathAtom inScope slashCommentParser boolean isTopMatter true javascript get extension() { return this.cue.replace("build", "") } buildOutput() { return this.root.compileTo(this.extension) } get outputFileNames() { return this.content?.split(" ") || [this.root.permalink.replace(".html", "." + this.extension.toLowerCase())] } async _buildFileType(extension, options) { const {root} = this const { fileSystem, folderPath, filename, filePath, path, lodash } = root const capitalized = lodash.capitalize(extension) const buildKeyword = "build" + capitalized const {outputFileNames} = this for (let name of outputFileNames) { try { await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized)) root.log(`š¾ Built ${name} from ${filename}`) } catch (err) { console.error(`Error while building '${filePath}' with extension '${extension}'`) throw err } } } abstractBuildOneCommandParser // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point. extends abstractBuildCommandParser javascript async buildOne(options) { await this._buildFileType(this.extension, options) } buildParsersParser popularity 0.000096 description Compile to Parsers file. extends abstractBuildOneCommandParser buildCsvParser popularity 0.000096 example buildCsv description Compile to CSV file. extends abstractBuildOneCommandParser buildTsvParser popularity 0.000096 description Compile to TSV file. extends abstractBuildOneCommandParser buildJsonParser popularity 0.000096 description Compile to JSON file. extends abstractBuildOneCommandParser abstractBuildTwoCommandParser extends abstractBuildCommandParser javascript async buildTwo(options) { await this._buildFileType(this.extension, options) } buildCssParser popularity 0.000048 description Compile to CSS file. extends abstractBuildTwoCommandParser example buildCss buildHtmlParser popularity 0.007645 description Compile to HTML file. extends abstractBuildTwoCommandParser boolean isPopular true javascript async buildTwo(options) { await this._copyExternalFiles(options) await super.buildTwo(options) } async _copyExternalFiles(options) { if (!this.isNodeJs()) return const {root} = this const externalFilesCopied = options.externalFilesCopied || {} // If this file uses a parser that has external requirements, // copy those from external folder into the destination folder. const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {} parsersRequiringExternals.forEach(parserId => { // If we have already copied external files for that parser return if (externalFilesCopied[folderPath][parserId]) return // If parser does not exist for some reason return if (!parserIdIndex[parserId]) return parserIdIndex[parserId].map(particle => { const externalFiles = particle.copyFromExternal.split(" ") externalFiles.forEach(name => { const newPath = path.join(folderPath, name) // If we have already copied that file return. We use the same hash since parserIds wont conflict with name, the latter always having a "." if (externalFilesCopied[name]) return fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name))) root.log(`š¾ Copied external file needed by ${parserId} in ${filename} to ${name}`) externalFilesCopied[name] = true }) }) if (parserId !== "scrollThemeParser") // todo: this is a hack that we dont cache scrollThemeParser since the external files // may be dynamic. externalFilesCopied[folderPath][parserId] = true }) } buildJsParser description Compile to JS file. extends abstractBuildTwoCommandParser buildRssParser popularity 0.000048 description Compile to RSS file. extends abstractBuildTwoCommandParser buildTxtParser popularity 0.007596 description Compile to TXT file. extends abstractBuildTwoCommandParser boolean isPopular true buildConceptsParser popularity 0.000024 cueFromId description Compile concepts to delimited files. extends abstractBuildCommandParser sortByParser cueFromId atoms cueAtom columnNameAtom javascript buildOutput() { // For testing return this.root.compileConcepts(this.outputFiles[0], this.sortBy) } get sortBy() { return this.get("sortBy") } get outputFiles() { const {permalink} = this.root const files = this.getAtomsFrom(1) if (!files.length) files.push(permalink.replace(".html", ".csv")) return files } async buildOne() { const {root, sortBy, outputFiles} = this const { fileSystem, folderPath, filename, path, concepts } = root const conceptCount = concepts.length for (let link of outputFiles) { await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy)) root.log(`š¾ Built ${conceptCount} concepts in ${filename} to ${link}`) } } fetchParser description Download URL to disk. extends abstractBuildCommandParser cueFromId atoms parseTimeCommandAtom urlAtom example fetch https://breckyunits.com/posts.csv fetch https://breckyunits.com/posts.csv renamed.csv javascript get url() { return this.getAtom(1) } get filename() { return this.getAtom(2) } async load() { await this.root.fetch(this.url, this.filename) } buildMeasuresParser popularity 0.000024 cueFromId description Compile measures to delimited files. extends abstractBuildCommandParser sortByParser cueFromId atoms cueAtom columnNameAtom javascript async buildOne() { const {root} = this const { fileSystem, folderPath, filename, path, permalink, measures } = root const files = this.getAtomsFrom(1) if (!files.length) files.push(permalink.replace(".html", ".csv")) const sortBy = this.get("sortBy") for (let link of files) { await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy)) root.log(`š¾ Built ${measures.length} measures in ${filename} to ${link}`) } } buildPdfParser popularity 0.000096 description Compile to PDF file. extends abstractBuildCommandParser javascript async buildTwo(options) { if (!this.isNodeJs()) return "Only works in Node currently." const {root} = this const { filename } = root const outputFile = root.filenameNoExtension + ".pdf" // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22 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}"` // console.log(`Node.js is running on architecture: ${process.arch}`) try { const output = require("child_process").execSync(command, { stdio: "ignore" }) root.log(`š¾ Built ${outputFile} from ${filename}`) } catch (error) { console.error(error) } } abstractInlineFileParser extends abstractScrollParser catchAllAtomType filePathAtom catchAllParser scrollFileAddressParser string joinChar ;\n\n string htmlTag script javascript get files() { const inline = this.atoms.slice(1) const children = this.map(particle => particle.cue) return inline.concat(children) } get contents() { return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar) } buildHtml() { return `<${this.htmlTag}>/* ${this.files.join(" ")} */\n${this.contents}</${this.htmlTag}>` } scrollInlineCssParser description Inline CSS from files. popularity 0.007211 extends abstractInlineFileParser cue inlineCss string joinChar \n\n string htmlTag style javascript buildCss() { return this.contents } scrollInlineJsParser description Inline JS from files. popularity 0.007211 extends abstractInlineFileParser cue inlineJs javascript buildJs() { return this.contents } abstractTopLevelSingleMetaParser description Use these parsers once per file. extends abstractScrollParser inScope slashCommentParser cueFromId atoms metaCommandAtom boolean isTopMatter true boolean isSetterParser true testStrictParser description Make catchAllParagraphParser = error. extends abstractTopLevelSingleMetaParser scrollDateParser cue date popularity 0.006680 catchAllAtomType dateAtom description Set published date. extends abstractTopLevelSingleMetaParser boolean isPopular true example date 1/11/2019 printDate Hello world dateline abstractUrlSettingParser extends abstractTopLevelSingleMetaParser atoms metaCommandAtom urlAtom cueFromId editBaseUrlParser popularity 0.007838 description Override edit link baseUrl. extends abstractUrlSettingParser canonicalUrlParser description Override canonical URL. extends abstractUrlSettingParser openGraphImageParser popularity 0.000796 // https://ogp.me/ // If not defined, Scroll will try to generate it's own using the first image tag on your page. description Override Open Graph Image. extends abstractUrlSettingParser baseUrlParser popularity 0.009188 description Required for RSS and OpenGraph. extends abstractUrlSettingParser example baseUrl https://hub.scroll.pub/ rssFeedUrlParser popularity 0.008850 description Set RSS feed URL. extends abstractUrlSettingParser editUrlParser catchAllAtomType urlAtom description Override edit link. extends abstractTopLevelSingleMetaParser siteOwnerEmailParser popularity 0.001302 description Set email address for site contact. extends abstractTopLevelSingleMetaParser cue email atoms metaCommandAtom emailAddressAtom faviconParser popularity 0.001688 catchAllAtomType stringAtom cue favicon description Favicon file. example favicon logo.png metatags buildHtml extends abstractTopLevelSingleMetaParser scrollNoFormatParser description Don't format below this line. cue noFormat atoms parseTimeCommandAtom extends abstractTopLevelSingleMetaParser importOnlyParser popularity 0.033569 // This line will be not be imported into the importing file. description Don't build this file. cueFromId atoms parseTimeCommandAtom extends abstractTopLevelSingleMetaParser inlineMarkupsParser popularity 0.000024 description Set global inline markups. extends abstractTopLevelSingleMetaParser cueFromId example inlineMarkups * // Disable * for bold _ u // Make _ underline htmlLangParser atoms metaCommandAtom stringAtom // for the <html lang=""> tag. If not specified will be "en". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang description Override HTML lang attribute. extends abstractTopLevelSingleMetaParser openGraphDescriptionParser popularity 0.001688 catchAllAtomType stringAtom cue description description Description for SEO meta tag. extends abstractTopLevelSingleMetaParser openGraphKeywordsParser popularity 0.001688 catchAllAtomType stringAtom cue keywords description Keywords for SEO meta tag. extends abstractTopLevelSingleMetaParser permalinkParser popularity 0.000265 description Override output filename. extends abstractTopLevelSingleMetaParser atoms metaCommandAtom permalinkAtom scrollTagsParser popularity 0.006801 cue tags description Set tags. example tags All extends abstractTopLevelSingleMetaParser catchAllAtomType tagAtom scrollTitleParser popularity 0.007524 catchAllAtomType stringAtom cue title description Set title. example title Eureka printTitle extends abstractTopLevelSingleMetaParser boolean isPopular true scrollLinkTitleParser popularity 0.007524 catchAllAtomType stringAtom cue linkTitle description Text for links. example title My blog - Eureka linkTitle Eureka extends abstractTopLevelSingleMetaParser scrollChatParser popularity 0.000362 description A faux text chat conversation. catchAllParser chatLineParser cue chat extends abstractScrollParser example chat Hi š javascript buildHtml() { return this.map((line, index) => line.asString ? `<div style="text-align: ${index % 2 ? "right" : "left"};" class="scrollChat ${index % 2 ? "scrollChatRight" : "scrollChatLeft"}"><span>${line.asString}</span></div>` : "").join("") } buildTxt() { return this.subparticlesToString() } abstractDatatableProviderParser description A datatable. extends abstractScrollParser inScope scrollTableDataParser abstractTableOptionsParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser assertSilenceBelowErrorsParser javascript get visualizations() { return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml) } buildHtml(buildSettings) { return this.visualizations.map(particle => particle.buildHtml(buildSettings)) .join("\n") .trim() } buildTxt() { return this.visualizations.map(particle => particle.buildTxt()) .join("\n") .trim() } _coreTable get coreTable() { if (this._coreTable) return this._coreTable const {delimiter, delimitedData} = this return [] } get columnNames() { return [] } scrollTableParser extends abstractDatatableProviderParser popularity 0.002133 cue datatable example datatable printTable data year,count 1900,10 2000,122 2020,23 catchAllAtomType filePathAtom int atomIndex 1 javascript get delimiter() { const {filename} = this let delimiter = "" if (filename) { const extension = filename.split('?')[0].split(".").pop() if (extension === "json") delimiter = "json" if (extension === "particles") delimiter = "particles" if (extension === "csv") delimiter = "," if (extension === "tsv") delimiter = "\t" if (extension === "ssv") delimiter = " " if (extension === "psv") delimiter = "|" } if (this.get("delimiter")) delimiter = this.get("delimiter") else if (!delimiter) { const header = this.delimitedData.split("\n")[0] if (header.includes("\t")) delimiter = "\t" else if (header.includes(",")) delimiter = "," else delimiter = " " } return delimiter } get filename() { return this.getAtom(this.atomIndex) } parseJson() { const {delimitedData} = this let obj = JSON.parse(delimitedData) let rows = [] const dotPath = this.get("path") if (dotPath) { function getValueFromPath(obj, path) { return path.split('.').reduce((current, key) => current && current[key] !== undefined ? current[key] : undefined, obj ); } obj = getValueFromPath(obj, dotPath) } // Optimal case: Array of objects if (Array.isArray(obj)) { rows = obj} else if (!Array.isArray(obj) && typeof obj === "object") { // Case 2: Nested array under a key const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key])) if (arrayKey) rows = obj[arrayKey] else { rows = Object.keys(obj).map(key => {return { key, value: obj[key]}}) } } // Case 3: Array of primitive values else if (Array.isArray(obj) && obj.length && typeof obj[0] !== "object") { rows = obj.map(value => ({ value })) } this._columnNames = rows.length ? Object.keys(rows[0]) : [] this._coreTable = rows return rows } get coreTable() { if (this._coreTable) return this._coreTable const {delimiter, delimitedData} = this if (delimiter === "json") return this.parseJson() else if (delimiter === "particles") { const d3lib = this.root.d3 this._coreTable = d3lib.dsvFormat(",").parse(new Particle(delimitedData).asCsv, d3lib.autoType) } else { const d3lib = this.root.d3 this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType) } this._columnNames = this._coreTable.columns delete this._coreTable.columns return this._coreTable } get columnNames() { // init coreTable to set columns const coreTable = this.coreTable return this._columnNames } async load() { if (this.filename) await this.root.fetch(this.filename) } get fileContent() { return this.root.readSyncFromFileOrUrl(this.filename) } get delimitedData() { // json csv tsv if (this.filename) return this.fileContent const dataParticle = this.getParticle("data") if (dataParticle) return dataParticle.subparticlesToString() // if not dataparticle and no filename, check [permalink].csv if (this.isNodeJs()) return this.root.readFile(this.root.permalink.replace(".html", "") + ".csv") return "" } clocParser extends scrollTableParser description Output results of cloc as table. cue cloc string copyFromExternal .clocLangs.txt string delimiter , javascript get delimitedData() { const { execSync } = require("child_process") const results = execSync(this.command).toString().trim() const csv = results.split("\n\n").pop().replace(/,\"github\.com\/AlDanial.+/, "") // cleanup output return csv } get command(){ return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || ""}` } scrollDependenciesParser extends scrollTableParser description Get files this file depends on. cue dependencies string delimiter , javascript get delimitedData() { return `file\n` + this.root.dependencies.join("\n") } scrollDiskParser extends scrollTableParser description Output file into as datatable. cue disk string delimiter json javascript get delimitedData() { return this.isNodeJs() ? this.delimitedDataNodeJs : "" } get delimitedDataNodeJs() { const fs = require('fs'); const path = require('path'); const {folderPath} = this.root const folder = this.content ? path.join(folderPath, this.content) : folderPath function getDirectoryContents(dirPath) { const directoryContents = []; const items = fs.readdirSync(dirPath); items.forEach((item) => { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); directoryContents.push({ name: item, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, lastModified: stats.mtime }); }); return directoryContents; } return JSON.stringify(getDirectoryContents(folder)) } scrollIrisParser extends scrollTableParser description Iris dataset from R.A. Fisher. cue iris example iris printTable scatter x SepalLength y SepalWidth javascript delimitedData = this.constructor.iris vegaSampleDataParser extends scrollTableParser description Sample dataset from Vega. cue sampleData atoms cueAtom vegaDataSetAtom example sampleData zipcodes.csv printTable javascript get filename() { return "https://ohayo.scroll.pub/ohayo/packages/vega/datasets/" + this.content } quickTableParser popularity 0.000024 extends scrollTableParser atoms urlAtom pattern ^[^\s]+\.(tsv|csv|ssv|psv|json)(?:\?[^\s]*)?$ int atomIndex 0 javascript get dependencies() { return [this.cue]} scrollConceptsParser description Load concepts as datatable. extends abstractDatatableProviderParser cue concepts atoms cueAtom example concepts printTable javascript get coreTable() { return this.root.concepts } get columnNames() { return this.root.measures.map(col => col.Name) } abstractPostsParser description Load posts as datatable. extends abstractDatatableProviderParser cueFromId atoms cueAtom catchAllAtomType tagWithOptionalFolderAtom javascript async load() { const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath) const {fileSystem} = this.root for (let folderPath of dependsOn) { // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.scrollFileSystemIdNumber}'`) await fileSystem.getFusedFilesInFolder(folderPath, ".scroll") } } get tags() { return this.content?.split(" ") || [] } get files() { const thisFile = this.root.file // todo: we can include this file, but just not run asTxt const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile) return files } get coreTable() { if (this._coreTable) return this._coreTable this._coreTable = this.files.map(file => this.postToRow(file)) return this._coreTable } postToRow(file) { const {relativePath} = file const {scrollProgram} = file.file const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram const text = asTxt.replace(/(\t|\n)/g, " ").replace(/</g, "<") return { title, titleLink: relativePath + permalink, text, date, wordCount, minutes } } columnNames = "title titleLink text date wordCount minutes".split(" ") scrollPostsParser popularity 0.000024 cue posts description Posts as datatable. extends abstractPostsParser example // Print a search datatable: posts printTable tableSearch // Dump a CSV of blog: buildHtml buildCsv buildJson scrollPostsMetaParser popularity 0.000024 cue postsMeta description Post meta as datatable. extends abstractPostsParser javascript columnNames = ["date", "year", "title", "permalink", "authors", "tags", "wordCount", "minutes"] postToRow(file) { const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram return { date, year, title, permalink, authors, tags, wordCount, minutes } } printFeedParser popularity 0.000048 description Print group to RSS. extends abstractPostsParser example printFeed index printFeed cars/index buildRss javascript buildRss() { const {dayjs} = this.root const scrollPrograms = this.files.map(file => file.file.scrollProgram) const { title, baseUrl, description } = this.root return `<?xml version="1.0" encoding="ISO-8859-1" ?> <rss version="2.0"> <channel> <title>${title}</title> <link>${baseUrl}</link> <description>${description}</description> <lastBuildDate>${dayjs().format("ddd, DD MMM YYYY HH:mm:ss ZZ")}</lastBuildDate> <language>en-us</language> ${scrollPrograms.map(program => program.toRss()).join("\n")} </channel> </rss>` } buildTxt() { return this.buildRss() } printSourceParser popularity 0.000024 description Print source for files in group(s). extends printFeedParser example printSource index buildTxt source.txt javascript buildHtml() { const files = this.root.getFilesByTags(this.content).map(file => file.file) return `${files.map(file => file.scrollProgram).map(scrollProgram => scrollProgram.filePath + "\n " + scrollProgram.codeAtStart.replace(/\n/g, "\n ") ).join("\n")}` } printSiteMapParser popularity 0.000072 extends abstractPostsParser description Print text sitemap. example baseUrl http://test.com printSiteMap javascript buildHtml() { const { baseUrl } = this.root return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join("\n") } buildTxt() { return this.buildHtml() } get dependencies() { return this.files} codeParser popularity 0.001929 description A code block. catchAllParser lineOfCodeParser extends abstractScrollParser boolean isPopular true example code two = 1 + 1 javascript buildHtml() { return `<code class="scrollCodeBlock">${this.code.replace(/\</g, "<")}</code>` } buildTxt() { return "```\n" + this.code + "\n```" } get code() { return this.subparticlesToString() } cueFromId codeWithHeaderParser popularity 0.000169 cueFromId catchAllAtomType stringAtom extends codeParser example codeWithHeader math.py two = 1 + 1 javascript buildHtml() { return `<div class="codeWithHeader"><div class="codeHeader">${this.header}</div>${super.buildHtml()}</div>` } buildTxt() { return "```" + this.header + "\n" + this.code + "\n```" } get header() { return this.content } codeFromFileParser popularity 0.000169 cueFromId atoms cueAtom urlAtom extends codeWithHeaderParser example codeFromFile math.py javascript get code() { return this.root.readSyncFromFileOrUrl(this.content) } dumpConceptParser popularity 0.000169 cueFromId atoms cueAtom extends codeWithHeaderParser example dumpConcept javascript get code() { return new Particle(this.root.concept).toString() } get header() { return this.root.filename } prismParser popularity 0.001929 description A code block with syntax highlighting. catchAllParser lineOfCodeParser cueFromId extends codeWithHeaderParser string copyFromExternal .prism.css .prism.js string requireOnce <link rel="stylesheet" href=".prism.css"> <script defer src=".prism.js"></script> <script> document.addEventListener("DOMContentLoaded", () => { if (typeof Prism !== 'undefined') { Prism.highlightAll(); } }); </script> example prism javascript const hello = "world"; console.log(hello); javascript buildHtml(buildSettings) { const content = this.content || "text" const language = content.split(".").pop() const header = content.includes(".") ? content : "" const languageClass = `language-${language}`; const prefix = this.getHtmlRequirements(buildSettings) const body = `<pre><code class="${languageClass}">${this.code.replace(/\</g, "<")}</code></pre>` if (!header) return prefix + body return prefix + `<div class="codeWithHeader"><div class="codeHeader">${header}</div>` + body + "</div>" } buildTxt() { return "```" + (this.content || "") + "\n" + this.code + "\n```"; } prismFromFileParser cueFromId atoms cueAtom urlAtom extends prismParser example prismFromFile math.py javascript get code() { return this.root.readSyncFromFileOrUrl(this.content) } get dependencies() { return [this.cue]} async load() { await this.root.fetch(this.content) } codeWithLanguageParser popularity 0.000458 description Use this to specify the language of the code block, such as csvCode or rustCode. extends codeParser pattern ^[a-zA-Z0-9_]+Code$ debugParsersParser description Print source code of all parsers used. extends codeParser cueFromId javascript buildParsers() { return this.code} get code() { return this.root.definition.toString() } debugSourceMapParser description Print the source map. extends codeParser cueFromId javascript get code() { const {root} = this const {file, filePath} = root return this.makeSourceMap(filePath, root.toString()) } makeSourceMap(fileName, fusedCode) { // Note this has tight integration with import parsers. If we change those, need to update this. const fileStack = [{ fileName, lineNumber: 0, linesLeft: fusedCode.split("\n").length }] return new Particle(fusedCode) .map(particle => { const currentFile = fileStack[fileStack.length - 1] currentFile.lineNumber++ currentFile.linesLeft-- if (particle.cue === "imported") { const linesLeft = parseInt(particle.get("particles")) const original = particle.get("original") fileStack.push({ fileName: particle.atoms[1], lineNumber: 0, linesLeft }) return `${currentFile.fileName}:${currentFile.lineNumber} ${original}\n` + particle.map(line => `${currentFile.fileName}:${currentFile.lineNumber} ${line}`).join("\n") } if (!currentFile.linesLeft) fileStack.pop() return particle .toString() .split("\n") .map((line, index) => { if (index) { currentFile.lineNumber++ currentFile.linesLeft-- } return `${currentFile.fileName}:${currentFile.lineNumber} ${line}` }) .join("\n") }) .join("\n") } abstractScrollWithRequirementsParser extends abstractScrollParser cueFromId javascript buildHtml(buildSettings) { return this.getHtmlRequirements(buildSettings) + this.buildInstance() } copyButtonsParser popularity 0.001471 extends abstractScrollWithRequirementsParser description Copy code widget. javascript buildInstance() { return "" } string requireOnce <script> document.addEventListener("DOMContentLoaded", () => document.querySelectorAll(".scrollCodeBlock").forEach(block => { if (!navigator.clipboard) return const button = document.createElement("span") button.classList.add("scrollCopyButton") block.appendChild(button) button.addEventListener("click", async () => { await navigator.clipboard.writeText(block.innerText) button.classList.add("scrollCopiedButton") }) } )) </script> abstractTableVisualizationParser extends abstractScrollWithRequirementsParser boolean isTableVisualization true javascript get columnNames() { return this.parent.columnNames } heatrixParser cueFromId example heatrix '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56 description A heatmap matrix data visualization. catchAllParser heatrixCatchAllParser extends abstractTableVisualizationParser javascript buildHtml() { // A hacky but simple way to do this for now. const advanced = new Particle("heatrixAdvanced") advanced.appendLineAndSubparticles("datatable", "\n " + this.tableData.replace(/\n/g, "\n ")) const particle = this.appendSibling("heatrixAdvanced", advanced.subparticlesToString()) const html = particle.buildHtml() particle.destroy() return html } get tableData() { const {coreTable} = this.parent if (!coreTable) return this.subparticlesToString() let table = new Particle(coreTable).asSsv if (this.parent.cue === "transpose") { // drop first line after transpose const lines = table.split("\n") lines.shift() table = lines.join("\n") } // detect years and make strings const lines = table.split("\n") const yearLine = / \d{4}(\s+\d{4})+$/ if (yearLine.test(lines[0])) { lines[0] = lines[0].replace(/ /g, " '") table = lines.join("\n") } return table } heatrixAdvancedParser popularity 0.000048 cueFromId catchAllParser heatrixCatchAllParser extends abstractTableVisualizationParser description Advanced heatrix. example heatrix datatable %h10; '2007 '2008 '2009 12 4 323 scale #ebedf0 0 #c7e9c0 100 #a1d99b 400 #74c476 1600 javascript buildHtml() { class Heatrix { static HeatrixId = 0 uid = Heatrix.HeatrixId++ constructor(program) { const isDirective = atom => /^(f|l|w|h)\d+$/.test(atom) || atom === "right" || atom === "left" || atom.startsWith("http://") || atom.startsWith("https://") || atom.endsWith(".html") const particle = new Particle(program) this.program = particle const generateColorBinningString = (data, colors) => { const sortedData = [...data].sort((a, b) => a - b); const n = sortedData.length; const numBins = colors.length; // Calculate the indices for each quantile const indices = []; for (let i = 1; i < numBins; i++) { indices.push(Math.floor((i / numBins) * n)); } // Get the quantile values and round them const thresholds = indices.map(index => Math.round(sortedData[index])); // Generate the string let result = ''; colors.forEach((color, index) => { const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index]; result += `${color} ${threshold}\n`; }); return result.trim(); } const buildScale = (table) => { const numbers = table.split("\n").map(line => line.split(" ")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number)) const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32']; numbers.unshift(0) return generateColorBinningString(numbers, colors); } const table = particle.getParticle("datatable").subparticlesToString() const scale = particle.getParticle("scale")?.subparticlesToString() || buildScale(table) const thresholds = [] const colors = [] scale.split("\n").map((line) => { const parts = line.split(" ") thresholds.push(parseFloat(parts[1])) colors.push(parts[0]) }) const colorCount = colors.length const colorFunction = (value) => { if (isNaN(value)) return "" // #ebedf0 for (let index = 0; index < colorCount; index++) { const threshold = thresholds[index] if (value <= threshold) return colors[index] } return colors[colorCount - 1] } const directiveDelimiter = ";" const getSize = (directives, letter) => directives .filter((directive) => directive.startsWith(letter)) .map((dir) => dir.replace(letter, "") + "px")[0] ?? "" this.table = table.split("\n").map((line) => line .trimEnd() .split(" ") .map((atom) => { const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join("") const directivesInThisAtom = atom .split(directiveDelimiter) .filter(isDirective) const value = parseFloat(atoms) const label = atoms.includes("'") ? atoms.split("'")[1] : atoms const alignment = directivesInThisAtom.includes("right") ? "right" : directivesInThisAtom.includes("left") ? "left" : "" const color = colorFunction(value) const width = getSize(directivesInThisAtom, "w") const height = getSize(directivesInThisAtom, "h") const fontSize = getSize(directivesInThisAtom, "f") const lineHeight = getSize(directivesInThisAtom, "l") || height const link = directivesInThisAtom.filter(i => i.startsWith("http") || i.endsWith(".html"))[0] const style = { "background-color": color, width, height, "font-size": fontSize, "line-height": lineHeight, "text-align": alignment, } Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key]) return { value, label, style, link, } }) ) } get html() { const { program } = this const cssId = `#heatrix${this.uid}` const defaultWidth = "40px" const defaultHeight = "40px" const fontSize = "10px" const lineHeight = defaultHeight const style = `<style> .heatrixContainer { margin: auto; }.heatrixRow {white-space: nowrap;} ${cssId} .heatrixAtom { font-family: arial; border-radius: 2px; border: 1px solid transparent; display: inline-block; margin: 1px; text-align: center; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; } .heatrixAtom a { color: black; } ${cssId} .heatrixAtom{ width: ${defaultWidth}; height: ${defaultHeight}; font-size: ${fontSize}; line-height: ${lineHeight}; } </style>` const firstRow = this.table[0] return ( `<div class="heatrixContainer" id="heatrix${this.uid}">${style}` + this.table .map((row, rowIndex) => { if (!rowIndex) return "" const rowStyle = row[0].style return `<div class="heatrixRow heatrixRow${rowIndex}">${row .map((atom, columnIndex) => { if (!columnIndex) return "" const columnStyle = firstRow[columnIndex]?.style || {} let { value, label, style, link } = atom const extendedStyle = Object.assign( {}, rowStyle, columnStyle, style ) const inlineStyle = Object.keys(extendedStyle) .map((key) => `${key}:${extendedStyle[key]};`) .join("") let valueClass = value ? " valueAtom" : "" const href = link ? ` href="${link}"` : "" return `<div class="heatrixAtom heatrixColumn${columnIndex}${valueClass}" style="${inlineStyle}"><a title="${label}" ${href}>${label}</a></div>` }) .join("")}</div>` }) .join("\n") + "</div>" ).replace(/\n/g, "") } } return new Heatrix(this.subparticlesToString().trim()).html } mapParser latParser atoms cueAtom floatAtom cueFromId single longParser atoms cueAtom floatAtom cueFromId single tilesParser atoms cueAtom tileOptionAtom cueFromId single zoomParser atoms cueAtom integerAtom cueFromId single geolocateParser description Geolocate user. atoms cueAtom cueFromId single radiusParser atoms cueAtom floatAtom cueFromId single fillOpacityParser atoms cueAtom floatAtom cueFromId single fillColorParser atoms cueAtom colorAtom cueFromId single colorParser atoms cueAtom colorAtom cueFromId single heightParser atoms cueAtom floatAtom cueFromId single hoverParser atoms cueAtom catchAllAtomType colorAtom cueFromId single extends abstractTableVisualizationParser description Map widget. string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js string requireOnce <link rel="stylesheet" href=".leaflet.css"> <script src=".leaflet.js"></script> <script src=".scrollLibs.js"></script> javascript buildInstance() { const height = this.get("height") || 500 const id = this._getUid() const obj = this.toObject() const template = {} const style = height !== "full" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;` const strs = ["color", "fillColor"] const nums = ["radius", "fillOpacity"] strs.filter(i => obj[i]).forEach(i => template[i] = obj[i]) nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i])) const mapId = `map${id}` return `<div id="${mapId}" style="${style}"></div> <script> { if (!window.maps) window.maps = {} const moveToMyLocation = () => { if (!navigator.geolocation) return navigator.geolocation.getCurrentPosition((position) => { const { latitude, longitude } = position.coords window.maps.${mapId}.setView([latitude, longitude]) L.circleMarker([latitude, longitude], { fillOpacity: 0.8, radius: 20, weight: 4 }) .addTo(window.maps.${mapId}) }, () => {}) } const lat = ${this.get("lat") ?? 37.8} const long = ${this.get("long") ?? 4} if (${this.has("geolocate")}){ moveToMyLocation() } const zoomLevel = ${this.get("zoom") ?? 4} const hover = '${this.get("hover") || "<b>{title}</b><br>{description}"}' const template = ${JSON.stringify(template)} const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)} window.maps.${mapId} = L.map("map${id}").setView([lat, long], zoomLevel) const map = window.maps.${mapId} const tileOptions = { "default": { baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '<a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors' }, light: { baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', attribution: '<a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors <a href="https://carto.com/ attributions">CARTO</a>' }, } const {baseLayer, attribution} = tileOptions.${this.get("tiles") || "default"} L.tileLayer(baseLayer, { attribution, maxZoom: 19 }).addTo(map); points.forEach(point => { L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries( Object.entries(point).filter(([key, value]) => value !== null) )}) .addTo(map) .bindPopup(new Particle(point).evalTemplateString(hover)) }) } </script>` } abstractPlotParser // Observablehq extends abstractTableVisualizationParser string copyFromExternal .d3.js .plot.js string requireOnce <script src=".d3.js"></script> <script src=".plot.js"></script> example plot inScope abstractColumnNameParser widthParser cueFromId atoms cueAtom integerAtom heightParser cueFromId atoms cueAtom integerAtom titleParser extends abstractPlotLabelParser subtitleParser extends abstractPlotLabelParser captionParser extends abstractPlotLabelParser javascript buildInstance() { const id = "plot" + this._getUid() return `<div id="${id}"></div><script> { let loadChart = async () => { const data = ${this.dataCode} const get = (col, index ) => col !== "undefined" ? col : (index === undefined ? undefined : Object.keys(data[0])[index]) document.querySelector("#${id}").append(Plot.plot(${this.plotOptions})) } loadChart() } </script>` } get sortExpression() { const sort = this.get("sort") if (!sort) return "" let sort_expr = "" if (sort.startsWith("-")) { // Sort by a value descending const sortCol = sort.slice(1) sort_expr = `, sort: {x: "y", reverse: true}` } else if (sort.includes(" ")) { // Fixed order specified const order = sort.split(" ") sort_expr = `, sort: {x: (a,b) => { const order = ${JSON.stringify(order)}; return order.indexOf(a) - order.indexOf(b) }}` } else if (sort === "asc") { sort_expr = `, sort: {x: "x"}` } else if (sort === "desc") { sort_expr = `, sort: {x: "x", reverse: true}` } return sort_expr } get marks() { // just for testing purposes return `Plot.rectY({length: 10000}, Plot.binX({y: "count"}, {x: d3.randomNormal()}))` } get dataCode() { const {coreTable} = this.parent return `d3.csvParse(\`${new Particle(coreTable).asCsv}\`, d3.autoType)` } get plotOptions() { return `{ title: "${this.get("title") || ""}", subtitle: "${this.get("subtitle") || ""}", caption: "${this.get("caption") || ""}", symbol: {legend: ${this.has("symbol")}}, color: {legend: ${this.has("fill") || this.has("stroke")}}, grid: ${this.get("grid") !== "false"}, marks: [${this.marks}], width: ${this.get("width") || 640}, height: ${this.get("height") || 400}, }` } plotScatterplotParser cue scatterplot extends abstractPlotParser description Scatterplot Widget. example iris scatterplot javascript get marks() { const x = this.get("x") const y = this.get("y") const text = this.get("label") return `Plot.dot(data, { x: get("${x}", 0), y: get("${y}", 1), r: get("${this.get("radius")}"), fill: get("${this.get("fill")}"), tip: true${this.sortExpression}, symbol: get("${this.get("symbol")}")} ), Plot.text(data, {x: get("${x}",0), y: get("${y}", 1), text: "${text}", dy: -6, lineAnchor: "bottom"})` } plotBarchartParser cue barchart extends abstractPlotParser description Bar chart widget. example iris barchart javascript get marks() { const x = this.get("x") const y = this.get("y") const text = this.get("label") const fill = this.get("fill") return `Plot.barY(data, { x: get("${x}", 0), y: get("${y}", 1), fill: get("${fill}"), tip: true${this.sortExpression} }), Plot.ruleY([0])` } plotLineChartParser cue linechart extends abstractPlotParser description Line chart widget. example iris linechart x SepalLength y SepalWidth javascript get marks() { const x = this.get("x") const y = this.get("y") const stroke = this.get("stroke") || "steelblue" const strokeWidth = this.get("strokeWidth") || 2 const strokeLinecap = this.get("strokeLinecap") || "round" const fill = this.get("fill") return `Plot.line(data, { x: get("${x}", 0), y: get("${y}", 1), stroke: "${stroke}", fill: get("${fill}"), strokeWidth: ${strokeWidth}, strokeLinecap: "${strokeLinecap}"${this.sortExpression} })` } sparklineParser popularity 0.000024 description Sparkline widget. extends abstractTableVisualizationParser example sparkline 1 2 3 4 5 string copyFromExternal .sparkline.js string requireOnce <script src=".sparkline.js"></script> catchAllAtomType numberAtom // we need pattern matching inScope scrollYParser javascript buildInstance() { const id = "spark" + this._getUid() const {columnValues} = this const start = this.has("start") ? parseInt(this.get("start")) : 0 const width = this.get("width") || 100 const height = this.get("height") || 30 const lineColor = this.get("color") || "black" return `<span id="${id}"></span><script>new Sparkline(document.getElementById("${id}"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: "${lineColor}", tooltip: (value,index) => ${start} + index + ": " + value}).draw(${JSON.stringify(columnValues)})</script>` } get columnValues() { if (this.content) return this.content.split(" ").map(str => parseFloat(str)) const {coreTable} = this.parent if (coreTable) { const columnName = this.get("y") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number') return coreTable.map(row => row[columnName]) } } printColumnParser popularity 0.000024 description Print one column extends abstractTableVisualizationParser example printColumn tags catchAllAtomType columnNameAtom joinParser boolean allowTrailingWhitespace true cueFromId atoms cueAtom catchAllAtomType stringAtom javascript buildHtml() { return this.columnValues.join(this.join) } buildTxt() { return this.columnValues.join(this.join) } get join() { return this.get("join") || "\n" } get columnName() { return this.atoms[1] } get columnValues() { return this.parent.coreTable.map(row => row[this.columnName]) } printTableParser popularity 0.001085 cueFromId description Print datatable. extends abstractTableVisualizationParser javascript get tableHeader() { return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\n`) } get columnNames() { return this.parent.columnNames } buildJson() { return JSON.stringify(this.coreTable, undefined, 2) } buildCsv() { return new Particle(this.coreTable).asCsv } buildTsv() { return new Particle(this.coreTable).asTsv } get columns() { const {columnNames} = this return columnNames.map((name, index) => { const isLink = name.endsWith("Link") const linkIndex = columnNames.indexOf(name + "Link") return { name, isLink, linkIndex } }) } toRow(row) { const {columns} = this const atoms = columns.map(col => row[col.name]) let str = "" let column = 0 const columnCount = columns.length while (column < columnCount) { const col = columns[column] column++ const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(" ") : atoms[column - 1]) ?? "").toString() if (col.isLink) continue const isTimestamp = col.name.toLowerCase().includes("time") && /^\d{10}(\d{3})?$/.test(content) const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content let tagged = text const link = atoms[col.linkIndex] const isUrl = content.match(/^https?\:[^ ]+$/) if (col.linkIndex > -1 && link) tagged = `<a href="${link}">${text}</a>` else if (col.name.endsWith("Url")) tagged = `<a href="${content}">${col.name.replace("Url", "")}</a>` else if (isUrl) tagged = `<a href="${content}">${text}</a>` str += `<td>${tagged}</td>\n` } return str } get coreTable() { return this.parent.coreTable } get tableBody() { return this.coreTable .map(row => `<tr>${this.toRow(row)}</tr>`) .join("\n") } buildHtml() { return `<table id="table${this._getUid()}" class="scrollTable"> <thead><tr>${this.tableHeader.join("\n")}</tr></thead> <tbody>${this.tableBody}</tbody> </table>` } buildTxt() { return this.parent.delimitedData || new Particle(this.coreTable).asCsv } katexParser popularity 0.001592 extends abstractScrollWithRequirementsParser catchAllAtomType codeAtom catchAllParser lineOfCodeParser example katex \text{E} = \text{T} / \text{A}! description KaTex widget for typeset math. string copyFromExternal .katex.min.css .katex.min.js string requireOnce <link rel="stylesheet" href=".katex.min.css"> <script defer src=".katex.min.js"></script> <script> document.addEventListener("DOMContentLoaded", () => document.querySelectorAll(".scrollKatex").forEach(el => { katex.render(el.innerText, el, { throwOnError: false }); } )) </script> javascript buildInstance() { const id = this._getUid() const content = this.content === undefined ? "" : this.content return `<div class="scrollKatex" id="${id}">${content + this.subparticlesToString()}</div>` } buildTxt() { return ( this.content ? this.content : "" )+ this.subparticlesToString() } helpfulNotFoundParser popularity 0.000048 extends abstractScrollWithRequirementsParser catchAllAtomType filePathAtom string copyFromExternal .helpfulNotFound.js description Helpful not found widget. javascript buildInstance() { return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id="helpfulNotFound"></h1><script defer src="/.helpfulNotFound.js"></script><script>document.addEventListener("DOMContentLoaded", () => new NotFoundApp('${this.content}'))</script>` } 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 `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=".jquery-3.7.1.min.js"></script><div class="slideshowNav"></div><script defer src=".slideshow.js"></script>` } tableSearchParser popularity 0.000072 extends abstractScrollWithRequirementsParser string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js string requireOnce <script defer src=".jquery-3.7.1.min.js"></script> <style>.dt-search{font-family: "SF Pro", "Helvetica Neue", "Segoe UI", "Arial";}</style> <link rel="stylesheet" href=".datatables.css"> <script defer src=".datatables.js"></script> <script defer src=".dayjs.min.js"></script> <script defer src=".tableSearch.js"></script> // adds to all tables on page description Table search and sort widget. javascript buildInstance() { return "" } abstractCommentParser description Prints nothing. catchAllAtomType commentAtom atoms commentAtom extends abstractScrollParser baseParser blobParser string bindTo next catchAllParser commentLineParser commentParser popularity 0.000193 extends abstractCommentParser cueFromId counterpointParser description Counterpoint comment. Prints nothing. extends commentParser cue ! slashCommentParser popularity 0.005643 extends abstractCommentParser cue // boolean isPopular true description A comment. Prints nothing. thanksToParser description Acknowledgements comment. Prints nothing. extends abstractCommentParser cueFromId loadConceptsParser // todo: clean this up. just add smarter imports with globs? // this currently removes first line of each (to ditch the imports). description Import all concepts in a folder. extends abstractScrollParser cueFromId atoms parseTimeCommandAtom filePathAtom javascript async wake() { const { Disk, path } = this.root const folder = path.join(this.root.folderPath, this.getAtom(1)) const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(".scroll")).map(Disk.read).map(content => content.split("\n").slice(1).join("\n")).join("\n\n") this.parent.concat(ONE_BIG_FILE) } conceptTemplateParser extends abstractScrollParser description Loads a template after document is parsed. example conceptTemplate # This is a page for {name} His phone is {phone} cueFromId javascript async wake() { this.root.notifyOnReady(this) } onReady() { if (this.root.has("importOnly")) return // Don't run on import only pages. const concept = this.root.concept const templateString = new Particle(this.subparticlesToString()) const newCode = templateString.templateToString(concept) this.replaceWith(newCode) } scrollClearStackParser popularity 0.000096 cue clearStack description Clear body stack. extends abstractScrollParser boolean isHtml true boolean noSnippet true javascript buildHtml() { return this.root.clearBodyStack().trim() } cssParser popularity 0.007211 extends abstractScrollParser description A style tag. example css body { color: red;} cueFromId catchAllParser cssLineParser catchAllAtomType cssAnyAtom javascript buildHtml() { return `<style>${this.css}</style>` } get css() { return this.content ?? this.subparticlesToString() } buildCss() { return this.css } scrollBackgroundColorParser description Quickly set CSS background. popularity 0.007211 extends abstractScrollParser cue background catchAllAtomType cssAnyAtom example background red javascript buildHtml() { return `<style>html, body { background: ${this.content};}</style>` } scrollFontColorParser description Quickly set CSS font-color. popularity 0.007211 extends abstractScrollParser cue color catchAllAtomType cssAnyAtom javascript buildHtml() { return `<style>html, body { color: ${this.content};}</style>` } scrollFontParser description Quickly set font family. popularity 0.007211 extends abstractScrollParser cue font atoms cueAtom fontFamilyAtom catchAllAtomType cssAnyAtom example font Slim javascript buildHtml() { const font = this.content === "Slim" ? "Helvetica Neue; font-weight:100;" : this.content return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>` } abstractQuickIncludeParser popularity 0.007524 extends abstractScrollParser atoms urlAtom javascript get dependencies() { return [this.filename]} get filename() { return this.getAtom(0) } quickCssParser popularity 0.007524 description Make a CSS tag. extends abstractQuickIncludeParser example style.css atoms urlAtom pattern ^[^\s]+\.(css)(?:\?[^\s]*)?$ boolean noSnippet true javascript buildHtml() { return `<link rel="stylesheet" type="text/css" href="${this.filename}">` } quickIncludeHtmlParser popularity 0.007524 description Include an HTML file. extends abstractQuickIncludeParser example body.html atoms urlAtom pattern ^[^\s]+\.(html|htm)(?:\?[^\s]*)?$ javascript buildHtml() { return this.root.readFile(this.filename) } quickScriptParser popularity 0.007524 description Make a Javascript tag. extends abstractQuickIncludeParser atoms urlAtom example script.js pattern ^[^\s]+\.(js)(?:\?[^\s]*)?$ boolean noSnippet true string attr javascript buildHtml() { return `<script ${this.attr} src="${this.filename}"></script>` } quickModuleScriptParser description Make a Javascript module tag. extends quickScriptParser example script.mjs pattern ^[^\s]+\.(mjs)(?:\?[^\s]*)?$ string attr type="module" scrollStylesParser cue styles extends abstractScrollParser description Write CSS with Particle Syntax. inScope abstractCommentParser catchAllParser selectorParser javascript getSelector() { return "" } get css() { return this.topDownArray .filter(particle => particle.isSelectorParser) .map(subparticle => subparticle.compile()) .join("") } buildHtml() { return `<style>${this.css}</style>` } buildCss() { return this.css } example styles .styleTest h1 color blue div class styleTest h1 Hello world propertyParser catchAllAtomType cssValueAtom catchAllParser errorParser javascript compile(spaces) { return `${spaces}${this.cue}: ${this.content};` } atoms cssPropertyNameAtom variableParser extends propertyParser pattern -- browserPrefixPropertyParser extends propertyParser pattern ^\-\w.+ atoms cssVendorPrefixCueAtom selectorParser inScope propertyParser variableParser commentParser catchAllParser selectorParser boolean isSelectorParser true javascript getSelector() { const parentSelector = this.parent.getSelector() return this.cue .split(",") .map(part => { if (part.startsWith("&")) return parentSelector + part.substr(1) return parentSelector ? parentSelector + " " + part : part }) .join(",") } compile() { const propertyParsers = this.getSubparticles().filter(particle => particle.doesExtend("propertyParser")) if (!propertyParsers.length) return "" const spaces = " " return `${this.getSelector()} { ${propertyParsers.map(subparticle => subparticle.compile(spaces)).join("\n")} }\n` } atoms cssSelectorAtom scrollDashboardParser popularity 0.000145 description Key stats in large font. catchAllParser lineOfCodeParser cue dashboard extends abstractScrollParser example dashboard #2 Popularity 30 Years Old $456 Revenue javascript get tableBody() { const items = this.topDownArray let str = "" for (let i = 0; i < items.length; i = i + 3) { str += this.makeRow(items.slice(i, i + 3)) } return str } makeRow(items) { return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join("\n") + `</tr>\n` } buildHtml() { return `<table class="scrollDashboard">${this.tableBody}</table>` } buildTxt() { return this.subparticlesToString() } belowAsCodeParser popularity 0.000651 description Print code below. string bindTo next extends abstractScrollParser catchAllAtomType integerAtom example belowAsCode iris printTable cueFromId string method next boolean reverse false javascript get selectedParticles() { const { method } = this let code = "" let particles = [] let next = this[method] let {howMany} = this while (howMany) { particles.push(next) next = next[method] howMany-- } if (this.reverse) particles.reverse() return particles } get code() { return this.selectedParticles.map(particle => particle.asString).join("\n") } buildHtml() { return `<code class="scrollCodeBlock">${this.code.replace(/\</g, "<")}</code>` } get howMany() { let howMany = parseInt(this.getAtom(1)) if (!howMany || isNaN(howMany)) howMany = 1 return howMany } debugBelowParser description Inspect particle below. extends belowAsCodeParser string copyFromExternal .debug.css javascript get code() { const mapFn = particle => { const atomTypes = particle.lineAtomTypes.split(" ") return `<div class="debugParticle"><span class="debugParticleId">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class="debugAtom">${atom}<span class="debugAtomType">${atomTypes[index]}</span></span>`).join(" ")}${(particle.length ? `<br><div class="debugSubparticles">` + particle.map(mapFn).join("<br>") + `</div>` : "")}</div>`} return this.selectedParticles.map(mapFn).join("<br>") } buildHtml() { return `<link rel="stylesheet" href=".debug.css">` + this.code } buildTxt() { return this.root.inspect(this.selectedParticles) } buildParsers() {return this.buildTxt()} debugAboveParser description Inspect particle above. extends debugBelowParser string bindTo previous string method previous boolean reverse true debugAllParser description Inspect entire document. extends debugBelowParser javascript get selectedParticles() { return this.root.getSubparticles()} debugTestParser description Inspect entire document during test. extends debugAllParser example // Test with "scroll test" # Hello world debugTest javascript getErrors() { console.log(this.buildTxt()) return super.getErrors() } belowAsCodeUntilParser description Print code above until match. extends belowAsCodeParser catchAllAtomType codeAtom example belowAsCode counter 1 second javascript get howMany() { let howMany = 1 const query = this.content let particle = this.next while (particle !== this) { if (particle.getLine().startsWith(query)) return howMany particle = particle.next howMany++ } return howMany } aboveAsCodeParser popularity 0.000482 string bindTo previous description Print code above. example counter 1 second aboveAsCode extends belowAsCodeParser string method previous boolean reverse true belowAsHtmlParser extends belowAsCodeParser description Displays html output of next particle in a code block. cueFromId example belowAsHtml # Hello world javascript get code() { // Todo: fix the bug where this will prevent buildRequirements from triggering. Refactor the requireOnce code. return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join("\n") } aboveAsHtmlParser description Displays html output of previous particle in a code block. extends belowAsHtmlParser example # Hello world aboveAsHtml string method previous boolean reverse true scrollDefParser popularity 0.004244 description Parser short form. pattern ^[a-zA-Z0-9_]+Def[ ] extends abstractScrollParser atoms cueAtom measureTypeAtom catchAllAtomType stringAtom example urlDef url What is the URL? javascript buildParsers(index) { // Make the first def the id particle const idStuff = index ? "" : `boolean isMeasure true boolean isMeasureRequired true boolean isConceptDelimiter true` const description = this.getAtomsFrom(2).join(" ") const cue = this.cue.replace("Def", "") const sortIndex = 1 + index/10 const type = this.atoms[1] const ucFirst = type.substr(0, 1).toUpperCase() + type.substr(1) return `${cue}DefParser cue ${cue} extends abstract${ucFirst}MeasureParser description ${description} float sortIndex ${sortIndex} ${idStuff}`.trim() } hamlParser popularity 0.007524 description HTML tag via HAML syntax. extends abstractScrollParser atoms urlAtom catchAllAtomType stringAtom pattern ^%?[\w\.]+#[\w\.]+ * javascript get tag() { return this.atoms[0].split(/[#\.]/).shift().replace("%", "") } get htmlId() { const idMatch = this.atoms[0].match(/#([\w-]+)/) return idMatch ? idMatch[1] : "" } get htmlClasses() { return this.atoms[0].match(/\.([\w-]+)/g)?.map(cls => cls.slice(1)) || []; } buildHtml() { const {htmlId, htmlClasses, content, tag} = this this.parent.sectionStack.unshift(`</${tag}>`) const attrs = [htmlId ? ' id="' + htmlId + '"' : "", htmlClasses.length ? ' class="' + htmlClasses.join(" ") + '"' : ""].join(" ").trim() return `<${tag}${attrs ? " " + attrs : ""}>${content || ""}` } buildTxt() { return this.content } hamlTagParser // Match plain tags like %h1 extends hamlParser pattern ^%[^#]+$ abstractHtmlParser extends abstractScrollParser catchAllParser htmlLineParser catchAllAtomType htmlAnyAtom javascript buildHtml() { return `${this.content ?? ""}${this.subparticlesToString()}` } buildTxt() { return "" } htmlParser popularity 0.000048 extends abstractHtmlParser description HTML one liners or blocks. cueFromId htmlInlineParser popularity 0.005788 extends abstractHtmlParser atoms htmlAnyAtom boolean isHtml true example <h1>Inline HTML</h1> pattern ^< description Inline HTML. boolean isPopular true javascript buildHtml() { return `${this.getLine() ?? ""}${this.subparticlesToString()}` } scrollBrParser popularity 0.000096 cue br example br 2 description A break. extends abstractScrollParser catchAllAtomType integerAtom boolean isHtml true javascript buildHtml() { return `<br>`.repeat(parseInt(this.getAtom(1) || 1)) } iframesParser popularity 0.000121 cueFromId catchAllAtomType urlAtom extends abstractScrollParser description An iframe(s). example iframes frame.html javascript buildHtml() { return this.atoms.slice(1).map(url => `<iframe src="${url}" frameborder="0"></iframe>`).join("\n") } abstractCaptionedParser extends abstractScrollParser atoms cueAtom urlAtom inScope captionAftertextParser slashCommentParser cueFromId javascript buildHtml(buildSettings) { const caption = this.getParticle("caption") const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : "" const {figureWidth} = this const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : "" const float = this.has("float") ? `margin: 20px; float: ${this.get("float")};` : "" return `<figure class="scrollCaptionedFigure" style="${widthStyle + float}">${this.getFigureContent(buildSettings)}${captionFig}</figure>` } get figureWidth() { return this.get("width") } scrollImageParser cue image popularity 0.005908 description An img tag. boolean isPopular true extends abstractCaptionedParser int atomIndex 1 example image screenshot.png caption A caption. inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser javascript get dimensions() { const width = this.get("width") const height = this.get("height") if (width || height) return {width, height} if (!this.isNodeJs()) return {} const src = this.filename // If its a local image, get the dimensions and put them in the HTML // to avoid flicker if (src.startsWith("http:") || src.startsWith("https:")) return {} if (this._dimensions) return this._dimensions try { const sizeOf = require("image-size") const path = require("path") const fullImagePath = path.join(this.root.folderPath, src) this._dimensions = sizeOf(fullImagePath) return this._dimensions } catch (err) { console.error(err) } return {} } get figureWidth() { return this.dimensions.width } get filename() { return this.getAtom(this.atomIndex) } get dependencies() { return [this.filename]} getFigureContent(buildSettings) { const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? "") : "") + this.filename const {width, height} = this.dimensions let dimensionAttributes = width || height ? `width="${width}" height="${height}" ` : "" // Todo: can we reuse more code from aftertext? const className = this.has("class") ? ` class="${this.get("class")}" ` : "" const id = this.has("id") ? ` id="${this.get("id")}" ` : "" const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope(["scrollLinkParser"])) || linkRelativeToCompileTarget const target = this.has("target") ? this.get("target") : (this.has("link") ? "" : "_blank") return `<a href="${clickLink}" target="${target}" ${className} ${id}><img src="${linkRelativeToCompileTarget}" ${dimensionAttributes}loading="lazy"></a>` } buildTxt() { const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join("\n") return "[Image Omitted]" + (subparticles ? "\n " + subparticles.replace(/\n/g, "\n ") : "") } quickImageParser popularity 0.005788 extends scrollImageParser example screenshot.png atoms urlAtom pattern ^[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp)(?:\?[^\s]*)?$ int atomIndex 0 qrcodeParser extends abstractCaptionedParser description Make a QR code from a link. example qrcode https://scroll.pub javascript getFigureContent() { const url = this.atoms[1] const isNode = this.isNodeJs() if (isNode) { const {externalsPath} = this.root const path = require("path") const {qrcodegen, toSvgString} = require(path.join(externalsPath, ".qrcodegen.js")) const QRC = qrcodegen.QrCode; const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM); const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo return svg } return `Not yet supported in browser.` } youtubeParser popularity 0.000121 extends abstractCaptionedParser // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg description A YouTube video widget. example youtube https://www.youtube.com/watch?v=lO8blNtYYBA javascript getFigureContent() { const url = this.getAtom(1).replace("youtube.com/watch?v=", "youtube.com/embed/") return `<div class="scrollYouTubeHolder"><iframe class="scrollYouTubeEmbed" src="${url}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>` } youTubeParser extends youtubeParser tags deprecate // Deprecated. You youtube all lowercase. importParser description Import a file. popularity 0.007524 cueFromId atoms parseTimeCommandAtom extends abstractScrollParser catchAllAtomType filePathAtom int filenameAt 1 example import header.scroll javascript get filename() { return this.getAtom(this.filenameAt) } get fullpath() { const {fileSystem, filePath} = this.root return fileSystem.makeRelativePath(filePath, this.filename) } async wake() { const {fullpath} = this const {file} = this.root if (!file.importsFiles) file.importsFiles = new Set() file.importsFiles.add(fullpath) return await this._handleImport() } async _handleImport() { const hasCircular = await this._hasCircularImports(this.root.filePath, this.fullpath) if (hasCircular) return await this._handleCircularImport(hasCircular) const file = await this._getFile() const particleCount = file.scrollProgram.length if (!file.exists) return await this._handleFileDoesNotExist() const newParticle = new Particle(`imported ${this.fullpath}\n exists true\n original ${this.getLine()}\n particles ${particleCount}`) if (this.has("moveToFooter")) newParticle.particleAt(0).set("moveToFooter", "true") await this.root.appendFromStream(newParticle.toString()) // Don't import importOnly lines // todo: look at perf const block = new Particle(file.scrollProgram.toString()) block.delete("importOnly") await this.root.appendFromStream(block.toString()) this.destroy() } async _handleFileDoesNotExist() { const newParticle = new Particle(`imported ${this.fullpath}\n exists false\n original ${this.getLine()}\n particles 0`) await this.root.appendFromStream(newParticle.toString()) this.destroy() } async _handleCircularImport(hasCircular) { const newParticle = new Particle(`imported ${this.fullpath}\n exists true\n original ${this.getLine()}\n particles 0\n circularImportError Circular import error: ${hasCircular}`) await this.root.appendFromStream(newParticle.toString()) this.destroy() } async _hasCircularImports(importer, importee) { // if a.scroll requires a.scroll if (importer === importee) return `${importee} imports itself` const {fileSystem} = this.root // Now check the files that the importee is importing const importeeFile = await fileSystem.getFile(importee) if (!importeeFile.importsFiles) return false if (importeeFile.importsFiles.has(importer)) return `${importee} imports ${importer}` for (let importPath of importeeFile.importsFiles) { // console.log(`Checking if ${importPath} imports ${importer}`) if (await this._hasCircularImports(importPath, importer)) return `${importPath} and ${importer}` } return false } async _getFile() { const {fileSystem, file} = this.root const fileToImport = await fileSystem.getFile(this.fullpath) await fileToImport.singlePassFuse() return fileToImport } quickImportParser popularity 0.007524 description Import a Scroll file. extends importParser boolean isPopular true int filenameAt 0 inScope importToFooterParser abstractCommentParser atoms urlAtom pattern ^[^\s]+\.scroll$ example header.scroll parsersImportParser description Import a Parsers file. extends quickImportParser inScope abstractCommentParser pattern ^[^\s]+\.parsers$ example measures.parsers javascript async _handleImport() { const file = await this._getFile() if (!file.exists) return await this._handleFileDoesNotExist() // todo: cleanup const newp = this.replaceWith(`useParserPool ${this.fullpath}`) await newp[0].wake() } scrollImportedParser description Inserted at import pass. boolean suggestInAutocomplete false cue imported atoms parseTimeCommandAtom extends abstractScrollParser baseParser blobParser catchAllAtomType filePathAtom javascript async wake() { this.checkForFooterMoves() } get dependencies() { return [this.atoms[1]]} checkForFooterMoves() { // If a particle was imported with the "moveToFooter" subparticle set, then that means we want // to move those imported particles to the end of the document. if (!this.has("moveToFooter")) return this.root.notifyOnReady(this) } onReady() { // We move the imported particle along with the number of lines originally imported // to the bottom of the document. Note: I have not looked into perf yet. const particleCount = parseInt(this.get("particles")) const index = this.index const particles = this.root.slice(index, index + particleCount + 1) const str = particles.map(part => part.toString()).join("\n") this.root.appendBlocks(str) particles.forEach(particle => particle.destroy()) } getErrors() { if (this.has("circularImportError")) return [this.makeError(this.get("circularImportError"))] if (this.get("exists") === "false" && this.previous.getLine() !== "// optional") return [this.makeError(`File '${this.atoms[1]}' does not exist.`)] return [] } useParserPoolParser cueFromId description Load a cached parser pool. extends abstractScrollParser atoms cueAtom urlAtom javascript async wake() { this.switchParserPool(this.filename) } get filename() { return this.atoms[1]} get dependencies() { return [this.filename]} scriptParser extends abstractScrollParser description Print script tag. cueFromId catchAllParser scriptLineParser catchAllAtomType javascriptAnyAtom javascript buildHtml() { return `<script>${this.scriptContent}</script>` } get scriptContent() { return this.content ?? this.subparticlesToString() } buildJs() { return this.scriptContent } evalJsParser description Replace this with evaled JS. extends abstractScrollParser baseParser blobParser cueFromId atoms parseTimeCommandAtom catchAllAtomType javascriptAnyAtom example evalJs "# The sum of 1 + 1 is " + 1 + 1 javascript async wake() { try { const particle = this const script = (this.content === undefined ? "" : this.content) + "\n" + this.subparticlesToString() const evaled = eval(script) await this.root.appendFromStream(evaled) } catch (err) { console.error(err) } } jsonScriptParser popularity 0.007524 cueFromId description Include JSON and assign to window. extends abstractScrollParser atoms cueAtom urlAtom javascript buildHtml() { const varName = this.filename.split("/").pop().replace(".json", "") return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>` } get filename() { return this.getAtom(1) } scrollLeftRightButtonsParser popularity 0.006342 cue leftRightButtons description Previous and next nav buttons. extends abstractScrollParser boolean noSnippet true javascript buildHtml() { const { linkToPrevious, linkToNext } = this.root if (!linkToPrevious) return "" 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;}` return `<style>${style}</style><a class="keyboardNav doNotPrint" style="left:.5rem;" href="${linkToPrevious}"><</a><a class="keyboardNav doNotPrint" style="right:.5rem;" href="${linkToNext}">></a>` } keyboardNavParser popularity 0.007476 description Make left and right navigate files. extends abstractScrollParser cueFromId catchAllAtomType urlAtom boolean noSnippet true javascript buildHtml() { const {root} = this const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious const linkToNext = this.getAtom(2) ?? root.linkToNext const script = `<script>document.addEventListener('keydown', function(event) { if (document.activeElement !== document.body) return if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut const getLinks = () => document.getElementsByClassName("scrollKeyboardNav")[0].getElementsByTagName("a") if (event.key === "ArrowLeft") getLinks()[0].click() else if (event.key === "ArrowRight") getLinks()[1].click() });</script>` return `<div class="scrollKeyboardNav" style="display:none;"><a href="${linkToPrevious}">${linkToPrevious}</a> Ā· ${root.permalink} Ā· <a href="${linkToNext}">${linkToNext}</a>${script}</div>` } printUsageStatsParser popularity 0.000096 // todo: if we include the atom "Parser" in a cue, bad things seem to happen. description Parser usage stats for folder. extends abstractScrollParser cueFromId javascript get stats() { const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join("\n")).join("\n") const result = input.split('\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {}) const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}}) const sorted = this.root.lodash.sortBy(rows, "count").reverse() return "parserId uses\n" + sorted.map(row => `${row.atom} ${row.count}`).join('\n') } buildHtml() { // A hacky but simple way to do this for now. const particle = this.appendSibling("datatable") particle.appendLine("delimiter ") particle.appendLine("printTable") const dataParticle = particle.appendLine("data") dataParticle.setSubparticles(this.stats) const html = particle.buildHtml() particle.destroy() return html } buildTxt() { return this.stats } buildCsv() { return this.stats.replace(/ /g, ",") } printScrollLeetSheetParser popularity 0.000024 description Print Scroll parser leet sheet. extends abstractScrollParser tags experimental cueFromId javascript get parsersToDocument() { const clone = this.root.clone() clone.setSubparticles("") clone.appendLine("") const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text) atoms.push("blankline") // manually add blank line atoms.push("Catch All Paragraph.") // manually add catch all paragraph atoms.push("<h></h>") // manually add html atoms.sort() clone.setSubparticles(atoms.join("\n").replace(/blankline/, "")) // insert blank line in right spot return clone } sortDocs(docs) { return docs.map(particle => { const {definition} = particle const {id, description, isPopular, examples, popularity} = definition const tags = definition.get("tags") || "" if (tags.includes("deprecate") || tags.includes("experimental")) return null const category = this.getCategory(tags) const note = this.getNote(category) return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)} }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id)) } makeLink(examples, cue) { // if (!examples.length) console.log(cue) // find particles that need docs const example = examples.length ? examples[0].subparticlesToString() : cue const base = `https://try.scroll.pub/` const particle = new Particle() particle.appendLineAndSubparticles("scroll", "theme gazette\n" + example) return base + "#" + encodeURIComponent(particle.asString) } docToHtml(doc) { const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }` return `<style>${css}</style><div id="scrollLeetSheet">` + doc.map(obj => `<div class="${obj.category}"><a href="${this.makeLink(obj.examples, obj.id)}">${obj.isPopular ? "<b>" : ""}${obj.id}</a> ${obj.description}${obj.isPopular ? "</b>" : ""}${obj.note}</div>`).join("\n") + "</div>" } buildHtml() { return this.docToHtml(this.sortDocs(this.parsersToDocument)) } buildTxt() { return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join("\n") } getCategory(input) { return "" } getNote() { return "" } get docs() { const rows = this.sortDocs(this.parsersToDocument).map(obj => { const {id, isPopular, description, popularity, category, examples, cue} = obj const example = examples.length ? examples[0].subparticlesToString() : cue return { id, isPopular, description, popularity, example, category } }) return this.root.lodash.sortBy(rows, "isPopular") } buildJson() { return JSON.stringify(this.docs, undefined, 2) } buildCsv() { return new Particle(this.docs).asCsv } printparsersLeetSheetParser popularity 0.000024 // todo: fix parse bug when atom Parser appears in parserId extends printScrollLeetSheetParser tags experimental description Parsers leetsheet. javascript buildHtml() { return "<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + "<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>" + this.docToHtml(this.sortDocs(this.atomParsersToDocument)) } makeLink() { return "" } categories = "assemblePhase acquirePhase analyzePhase actPhase".split(" ") getCategory(tags) { return tags.split(" ").filter(w => w.endsWith("Phase"))[0] } getNote(category) { return ` <span class="note">A${category.replace("Phase", "").substr(1)}Time.</span>` } get atomParsersToDocument() { const parsersParser = require("scrollsdk/products/parsers.nodejs.js") const clone = new parsersParser("anyAtom\n ").clone() const parserParticle = clone.getParticle("anyAtom") const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text) atoms.sort() parserParticle.setSubparticles(atoms.join("\n")) return parserParticle } get parsersToDocument() { const parsersParser = require("scrollsdk/products/parsers.nodejs.js") const clone = new parsersParser("latinParser\n ").clone() const parserParticle = clone.getParticle("latinParser") const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text) atoms.sort() parserParticle.setSubparticles(atoms.join("\n")) clone.appendLine("myParser") clone.appendLine("myAtom") return parserParticle } abstractMacroParser extends abstractScrollParser catchAllAtomType stringAtom atoms parseTimeCommandAtom cueFromId javascript get search() { return this.atoms[1] } get replacement() { return this.value } get value() { return this.length ? this.subparticlesToString() : this.getAtomsFrom(2).join(" ") } wake() { const {search, replacement} = this this.addFn(search, replacement) } addFn(search, replacement) { // Note that we don't return replacement directly, because it may contain $& // which is a special string in JS replace. "a".replace("a", `$&`). // PLDB actually contains that sequence. this.root.addTransformer(block => block.replaceAll(search, () => replacement)) } replaceParser description Replace this with that. extends abstractMacroParser atoms parseTimeCommandAtom codeAtom baseParser blobParser example replace YEAR 2022 replaceJsParser description Replace this with evaled JS. extends replaceParser catchAllAtomType javascriptAnyAtom example replaceJs SUM 1+1 * 1+1 = SUM javascript get replacement() { const particle = this const codeAtStart = this.root.codeAtStart || "" try { return eval(super.replacement) } catch (err) { console.error(err) } return this.search } replaceNodejsParser description Replace with evaled Node.JS. extends abstractMacroParser catchAllAtomType javascriptAnyAtom baseParser blobParser example replaceNodejs module.exports = {SCORE : 1 + 2} * The score is SCORE javascript wake() { const replacements = this.root.evalNodeJs(this) if (replacements) Object.keys(replacements).forEach(needle => { this.addFn(needle, replacements[needle]) }) } abstractMeasureParser atoms measureNameAtom description Base parser all measures extend. cueFromId boolean isMeasure true float sortIndex 1.9 boolean isComputed false string typeForWebForms text extends abstractScrollParser javascript get measureValue() { return this.content ?? "" } get measureName() { return this.getCuePath().replace(/ /g, "_") } abstractAtomMeasureParser description Contains a single word. atoms measureNameAtom atomAtom example nicknameParser extends abstractAtomMeasureParser id Breck nickname breck extends abstractMeasureParser abstractEmailMeasureParser description Email address. example emailParser extends abstractEmailMeasureParser id Breck email breck7@gmail.com string typeForWebForms email atoms measureNameAtom emailAddressAtom extends abstractAtomMeasureParser abstractUrlMeasureParser description A single url. example homepageParser extends abstractUrlMeasureParser id Breck homepage https://breckyunits.com string typeForWebForms url atoms measureNameAtom urlAtom extends abstractAtomMeasureParser abstractStringMeasureParser description General text data with no specific format. catchAllAtomType stringAtom example titleParser extends abstractStringMeasureParser id Breck title I build languages for scientists of all ages extends abstractMeasureParser abstractIdParser cue id description What is the ID of this concept? extends abstractStringMeasureParser example idParser extends abstractIdParser id breck float sortIndex 1 boolean isMeasureRequired true boolean isConceptDelimiter true javascript getErrors() { const errors = super.getErrors() let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== "id") if (!requiredMeasureNames.length) return errors let next = this.next while (requiredMeasureNames.length && next.cue !== "id" && next.index !== 0) { requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue) next = next.next } requiredMeasureNames.forEach(name => errors.push(this.makeError(`Concept "${this.content}" is missing required measure "${name}".`)) ) return errors } abstractIdMeasureParser description Alias for abstractIdParser. extends abstractIdParser abstractTextareaMeasureParser string typeForWebForms textarea example bioParser extends abstractTextareaMeasureParser id Breck bio I build languages for scientists of all ages description Long-form text content with preserved line breaks. extends abstractMeasureParser baseParser blobParser javascript get measureValue() { return this.subparticlesToString().replace(/\n/g, "\\n") } abstractNumericMeasureParser string typeForWebForms number description Base number type. extends abstractMeasureParser javascript get measureValue() { const {content} = this return content === undefined ? "" : parseFloat(content) } abstractNumberMeasureParser description Alias to abstractNumericMeasureParser. extends abstractNumericMeasureParser abstractIntegerMeasureParser description An integer. example ageParser extends abstractIntegerMeasureParser id Breck age 40 atoms measureNameAtom integerAtom extends abstractNumericMeasureParser javascript get measureValue() { const {content} = this return content === undefined ? "" : parseInt(content) } abstractIntMeasureParser description Alias to abstractIntegerMeasureParser. extends abstractIntegerMeasureParser abstractFloatMeasureParser description A float. example temperatureParser extends abstractFloatMeasureParser id Breck temperature 31.8 atoms measureNameAtom floatAtom extends abstractNumericMeasureParser abstractPercentageMeasureParser description A percentage. atoms measureNameAtom percentAtom extends abstractNumericMeasureParser example ownershipParser extends abstractPercentageMeasureParser id Breck ownership 31.8 javascript get measureValue() { const {content} = this return content === undefined ? "" : parseFloat(content) } abstractEnumMeasureParser description A single enum. atoms measureNameAtom enumAtom extends abstractMeasureParser example favoriteHtmlTagParser extends abstractEnumMeasureParser atoms measureNameAtom htmlTagAtom id Breck favoriteHtmlTag 2020 abstractBooleanMeasureParser description A single boolean. atoms measureNameAtom booleanAtom extends abstractMeasureParser example hasBillOfRightsParser extends abstractBooleanMeasureParser id USA hasBillOfRights true javascript get measureValue() { const {content} = this return content === undefined ? "" : content == "true" } abstractBoolMeasureParser description Alias to abstractBooleanMeasureParser. extends abstractBooleanMeasureParser abstractDateMeasureParser description Year/month/day in ISO 8601, US, European formats. atoms measureNameAtom dateAtom extends abstractMeasureParser string typeForWebForms date javascript get measureValue() { const {content} = this if (!content) return "" const {dayjs} = this.root try { // First try parsing with dayjs const parsed = dayjs(content) if (parsed.isValid()) return parsed.format("YYYY-MM-DD") // Try parsing other common formats const formats = [ "MM/DD/YYYY", "DD/MM/YYYY", "YYYY/MM/DD", "MM-DD-YYYY", "DD-MM-YYYY", "YYYY-MM-DD", "DD.MM.YYYY", "YYYY.MM.DD" ] for (const format of formats) { const attempt = dayjs(content, format) if (attempt.isValid()) return attempt.format("YYYY-MM-DD") } } catch (err) { console.error(err) return "" } return "" } get valueAsTimestamp() { const {measureValue} = this return measureValue ? this.root.dayjs(measureValue).unix() : "" } metaTagsParser popularity 0.007693 cueFromId extends abstractScrollParser description Print meta tags including title. boolean noSnippet true javascript buildHtml() { const {root} = this const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root const rssFeedUrl = root.get("rssFeedUrl") const favicon = root.get("favicon") const faviconTag = favicon ? `<link rel="icon" href="${favicon}">` : "" const keywordsTag = keywords ? `<meta name="keywords" content="${keywords}">` : "" const rssTag = rssFeedUrl ? `<link rel="alternate" type="application/rss+xml" title="${title}" href="${rssFeedUrl}">` : "" const gitTag = gitRepo ? `<link rel="source" type="application/git" title="Source Code Repository" href="${gitRepo}">` : "" return `<head> <meta charset="utf-8"> <title>${title}</title> <script>/* This HTML was generated from ${filename} by š Scroll v${scrollVersion}. https://scroll.pub */</script> <style>@media print {.doNotPrint {display: none !important;}}</style> <link rel="canonical" href="${canonicalUrl}"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="description" content="${description}"> ${keywordsTag} <meta name="generator" content="Scroll v${scrollVersion}"> <meta property="og:title" content="${title}"> <meta property="og:description" content="${description}"> <meta property="og:image" content="${openGraphImage}"> ${faviconTag} ${gitTag} ${rssTag} <meta name="twitter:card" content="summary_large_image"> </head> <body>` } moveToFooterParser extends abstractScrollParser description Move a section to the footer. atoms parseTimeCommandAtom cueFromId javascript async wake() { this.root.notifyOnReady(this) } onReady() { this.section.forEach(particle => { const clone = particle.clone() this.root.appendParticle(clone) particle.destroy() }) this.destroy() return "" } evalNodejsParser description Replace with evaled Node.JS. extends abstractScrollParser catchAllAtomType javascriptAnyAtom baseParser blobParser atoms parseTimeCommandAtom cueFromId example evalNodejs module.exports = "# Hello world" javascript wake() { const evaled = this.root.evalNodeJs(this) if (evaled !== false) this.replaceWith(evaled) } quoteParser popularity 0.001471 cueFromId description A quote. catchAllParser quoteLineParser extends abstractScrollParser javascript buildHtml() { return `<blockquote class="scrollQuote">${this.subparticlesToString()}</blockquote>` } buildTxt() { return this.subparticlesToString() } redirectToParser popularity 0.000072 description HTML redirect tag. extends abstractScrollParser atoms cueAtom urlAtom cueFromId example redirectTo https://scroll.pub/releaseNotes.html javascript buildHtml() { return `<meta http-equiv="Refresh" content="0; url='${this.getAtom(1)}'" />` } runScriptParser popularity 0.000024 description Run script and dump stdout. extends abstractScrollParser atoms cueAtom urlAtom cue run int filenameIndex 1 javascript get dependencies() { return [this.filename]} results = "Not yet run" async execute() { if (!this.filename) return await this.root.fetch(this.filename) // todo: make async const { execSync } = require("child_process") this.results = execSync(this.command) } get command() { const path = this.root.path const {filename }= this const fullPath = this.root.makeFullPath(filename) const ext = path.extname(filename).slice(1) const interpreterMap = { php: "php", py: "python3", rb: "ruby", pl: "perl", sh: "sh" } return [interpreterMap[ext], fullPath].join(" ") } buildHtml() { return this.buildTxt() } get filename() { return this.getAtom(this.filenameIndex) } buildTxt() { return this.results.toString().trim() } quickRunScriptParser extends runScriptParser atoms urlAtom pattern ^[^\s]+\.(py|pl|sh|rb|php)$ int filenameIndex 0 endSnippetParser popularity 0.004293 description Cut for snippet here. extends abstractScrollParser cueFromId toStampParser description Print a directory to stamp with configurable filters. extends abstractScrollParser catchAllAtomType filePathAtom cueFromId gitOnlyParser description Only include git-tracked files atoms cueAtom single excludeParser description Exclude files/directories matching these glob patterns atoms cueAtom catchAllAtomType stringAtom maxDepthParser description Maximum directory depth to traverse atoms cueAtom integerAtom javascript buildTxt() { return this.makeStamp(this.content) } buildHtml() { return this.buildTxt() } makeStamp(dir) { const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); let stamp = 'stamp\n'; const gitOnly = this.has("gitOnly"); const excludePatterns = this.get("exclude")?.split(" ") || [".git/**", "node_modules/**"]; const maxDepth = parseInt(this.get("maxDepth")) || Infinity; const gitTrackedFiles = new Set( gitOnly ? execSync('git ls-files', { cwd: dir, encoding: 'utf-8' }) .split('\n') .filter(Boolean) : [] ); // Convert glob patterns to RegExp const excludeRegexes = excludePatterns.map(pattern => new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$') ); const shouldInclude = (relativePath, isDirectory) => { if (excludeRegexes.some(regex => regex.test(relativePath))) return false; if (isDirectory) return true if (gitOnly && !gitTrackedFiles.has(relativePath)) return false; return true; }; const handleFile = (indentation, relativePath, itemPath) => { if (!shouldInclude(relativePath)) return; stamp += `${indentation}${relativePath}\n`; try { const content = fs.readFileSync(itemPath, { encoding: 'utf8', flag: 'r' }); // Skip if file appears to be binary if (content.includes('\0') || /[\x00-\x08\x0E-\x1F]/.test(content)) return; stamp += `${indentation} ${content.replace(/\n/g, `\n${indentation} `)}\n`; } catch (err) { // Skip files that can't be read as utf8 console.error(`Error reading ${itemPath}: ${err.message}`); return; } }; function processDirectory(currentPath, depth = 0) { if (depth > maxDepth) return; const items = fs.readdirSync(currentPath); items.forEach(item => { const itemPath = path.join(currentPath, item); const relativePath = path.relative(dir, itemPath); const stats = fs.statSync(itemPath); const isDirectory = stats.isDirectory() if (!shouldInclude(relativePath, isDirectory)) return; const indentation = ' '.repeat(depth); if (isDirectory) { stamp += `${indentation}${relativePath}/\n`; processDirectory(itemPath, depth + 1); } else if (stats.isFile()) { handleFile(indentation, relativePath, itemPath); } }); } const stats = fs.statSync(dir); if (stats.isDirectory()) { processDirectory(dir, 1); } else { handleFile(" ", dir, dir); } return stamp.trim(); } stampParser description Expand project template to disk. extends abstractScrollParser inScope stampFolderParser catchAllParser stampFileParser example stamp .gitignore *.html readme.scroll # Hello world <script src="scripts/nested/hello.js"></script> scripts/ nested/ hello.js console.log("Hello world") cueFromId atoms parseTimeCommandAtom javascript execute() { const dir = this.root.folderPath this.forEach(particle => particle.execute(dir)) } plainTextParser description Plain text oneliner or block. cueFromId extends abstractScrollParser catchAllParser plainTextLineParser catchAllAtomType stringAtom javascript buildHtml() { return this.buildTxt() } buildTxt() { return `${this.content ?? ""}${this.subparticlesToString()}` } plainTextOnlyParser popularity 0.000072 extends plainTextParser description Only print for buildTxt. scrollThemeParser popularity 0.007524 boolean isPopular true cue theme example theme gazette extends abstractScrollParser catchAllAtomType scrollThemeAtom description A collection of simple themes. string copyFromExternal .gazette.css // Note this will be replaced at runtime javascript get copyFromExternal() { return this.files.join(" ") } get files() { return this.atoms.slice(1).map(name => `.${name}.css`).concat([".scroll.css"]) } buildHtml() { return this.files.map(name => `<link rel="stylesheet" type="text/css" href="${name}">`).join("\n") } abstractAftertextAttributeParser atoms cueAtom boolean isAttribute true description Set HTML attribute. javascript get htmlAttributes() { return `${this.cue}="${this.content}"` } aftertextIdParser popularity 0.000145 cue id description Provide an ID to be output in the generated HTML tag. extends abstractAftertextAttributeParser atoms cueAtom htmlIdAtom single aftertextStyleParser popularity 0.000217 cue style description Set HTML style attribute. extends abstractAftertextAttributeParser catchAllAtomType cssAnyAtom javascript htmlAttributes = "" // special case this one get css() { return `${this.property}:${this.content};` } aftertextFontParser popularity 0.000217 cue font description Set font. extends aftertextStyleParser atoms cueAtom fontFamilyAtom catchAllAtomType cssAnyAtom string property font-family javascript get css() { if (this.content === "Slim") return "font-family:Helvetica Neue; font-weight:100;" return super.css } aftertextColorParser popularity 0.000217 cue color description Set font color. extends aftertextStyleParser catchAllAtomType cssAnyAtom string property color example color blue aftertextRightParser cue right description Right align a paragraph. extends aftertextStyleParser example > Veni. Vidi. Vici. Julius caesar right string css text-align: right; aftertextHrefParser popularity 0.000217 cue href description Set HTML href attribute. extends abstractAftertextAttributeParser atoms cueAtom urlAtom aftertextSrcParser extends aftertextHrefParser cue src description Set HTML src attribute. aftertextOnclickParser popularity 0.000217 cue onclick description Set HTML onclick attribute. extends abstractAftertextAttributeParser catchAllAtomType javascriptAnyAtom aftertextHiddenParser cue hidden atoms cueAtom description Do not compile this particle to HTML. extends abstractAftertextAttributeParser single aftertextValueParser popularity 0.000217 cue value description Set HTML value attribute. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextForParser popularity 0.000217 cue for description Set HTML for attribute. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextPlaceholderParser popularity 0.000217 cue placeholder description Set HTML placeholder attribute. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextRowsParser popularity 0.000217 cue rows description Set HTML rows attribute. extends abstractAftertextAttributeParser atoms cueAtom integerAtom aftertextTypeParser popularity 0.000217 cue type description Set HTML type attribute. extends abstractAftertextAttributeParser atoms cueAtom htmlTypeAtom aftertextAltParser popularity 0.000217 cue alt description Set HTML alt attribute for images. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextTitleParser popularity 0.000217 cue title description Set HTML title attribute. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextNameParser popularity 0.000217 cue name description Set HTML name attribute. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextOnsubmitParser popularity 0.000217 cue onsubmit description Set HTML onsubmit attribute. extends abstractAftertextAttributeParser catchAllAtomType javascriptAnyAtom aftertextClassParser popularity 0.000217 cue class description Set HTML class attribute. extends abstractAftertextAttributeParser catchAllAtomType classNameAtom aftertextMaxlengthParser popularity 0.000217 cue maxlength description Set HTML maxlength attribute. extends abstractAftertextAttributeParser atoms cueAtom integerAtom aftertextPatternParser popularity 0.000217 cue pattern description Set HTML pattern attribute for input validation. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextRequiredParser popularity 0.000217 cue required description Set HTML required attribute. extends abstractAftertextAttributeParser single aftertextDisabledParser popularity 0.000217 cue disabled description Set HTML disabled attribute. extends abstractAftertextAttributeParser single aftertextReadonlyParser popularity 0.000217 cue readonly description Set HTML readonly attribute. extends abstractAftertextAttributeParser single aftertextNoSnippetParser cue noSnippet description Generate no HTML snippet. extends abstractAftertextAttributeParser single aftertextAriaLabelParser popularity 0.000217 cue aria-label description Set ARIA label for accessibility. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextTargetParser popularity 0.000217 cue target description Set HTML target attribute for links. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextContentParser popularity 0.000217 cue content description Set HTML content attribute. extends abstractAftertextAttributeParser catchAllAtomType htmlAnyAtom aftertextMinParser cue min extends abstractAftertextAttributeParser atoms cueAtom numberAtom aftertextMaxParser cue max extends abstractAftertextAttributeParser atoms cueAtom numberAtom aftertextStepParser cue step extends abstractAftertextAttributeParser atoms cueAtom numberAtom aftertextWidthParser cue width extends abstractAftertextAttributeParser catchAllAtomType cssAnyAtom aftertextHeightParser cue height extends abstractAftertextAttributeParser catchAllAtomType cssAnyAtom aftertextDataParser popularity 0.000217 pattern ^data\- description Set HTML data- attribute. extends abstractAftertextAttributeParser atoms cueAtom catchAllAtomType htmlAnyAtom example div My score data-score 100 aftertextRoleParser popularity 0.000217 cue role description Set ARIA role attribute for accessibility. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextTabindexParser popularity 0.000217 cue tabindex description Set tabindex attribute for keyboard navigation. extends abstractAftertextAttributeParser atoms cueAtom integerAtom aftertextColspanParser popularity 0.000217 cue colspan description Set colspan attribute for table cells. extends abstractAftertextAttributeParser atoms cueAtom integerAtom aftertextRowspanParser popularity 0.000217 cue rowspan description Set rowspan attribute for table cells. extends abstractAftertextAttributeParser atoms cueAtom integerAtom aftertextMethodParser popularity 0.000217 cue method description Set form method attribute (GET/POST). extends abstractAftertextAttributeParser atoms cueAtom htmlMethodAtom aftertextActionParser popularity 0.000217 cue action description Set form action URL. extends abstractAftertextAttributeParser atoms cueAtom urlAtom aftertextAutocompleteParser popularity 0.000217 cue autocomplete description Set form autocomplete attribute. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextMultipleParser popularity 0.000217 cue multiple description Allow multiple selections or files. extends abstractAftertextAttributeParser single aftertextSelectedParser popularity 0.000217 cue selected description Set selected state for options. extends abstractAftertextAttributeParser single aftertextCheckedParser popularity 0.000217 cue checked description Set checked state for inputs. extends abstractAftertextAttributeParser single aftertextRelParser popularity 0.000217 cue rel description Set rel attribute for links. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextEnctypeParser popularity 0.000217 cue enctype description Set form enctype for file uploads. extends abstractAftertextAttributeParser atoms cueAtom htmlAnyAtom aftertextAcceptParser popularity 0.000217 cue accept description Set accepted file types for file inputs. extends abstractAftertextAttributeParser catchAllAtomType mimeTypeAtom aftertextTagParser atoms cueAtom htmlTagAtom description Override the HTML tag that the compiled particle will use. cue tag abstractAftertextDirectiveParser atoms cueAtom catchAllAtomType stringAtom boolean isMarkup true javascript getErrors() { if (this.silenceErrors) return [] // todo: generalize this? const errors = super.getErrors() if (!this.isMarkup || this.matchWholeLine) return errors const inserts = this.getInserts(this.parent.originalTextPostLinkify) // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types. // todo: also need to be able to map lines back to their line in source (pre-imports) if (!inserts.length) errors.push(this.makeError(`No match found for "${this.getLine()}".`)) return errors } get pattern() { return this.getAtomsFrom(1).join(" ") } get shouldMatchAll() { return this.has("matchAll") } getMatches(text) { const { pattern } = this const escapedPattern = pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") return [...text.matchAll(new RegExp(escapedPattern, "g"))].map(match => { const { index } = match const endIndex = index + pattern.length return [ { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex }, { index: endIndex, endIndex, string: `</${this.closeTag}>` } ] }) } getInserts(text) { const matches = this.getMatches(text) if (!matches.length) return false if (this.shouldMatchAll) return matches.flat() const match = this.getParticle("match") if (match) return match.indexes .map(index => matches[index]) .filter(i => i) .flat() return matches[0] } get allAttributes() { const attr = this.attributes.join(" ") return attr ? " " + attr : "" } get attributes() { return [] } get openTag() { return this.tag } get closeTag() { return this.tag } abstractMarkupParser extends abstractAftertextDirectiveParser inScope abstractMarkupParameterParser int patternStartsAtAtom 1 javascript get matchWholeLine() { return this.getAtomsFrom(this.patternStartsAtAtom).length === 0 } get pattern() { return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(" ") } boldParser popularity 0.000096 cueFromId description Bold matching text. extends abstractMarkupParser string tag b italicsParser popularity 0.000241 cueFromId description Italicize matching text. extends abstractMarkupParser string tag i underlineParser popularity 0.000024 description Underline matching text. cueFromId extends abstractMarkupParser string tag u afterTextCenterParser popularity 0.000193 description Center paragraph. cue center extends abstractMarkupParser string tag center aftertextCodeParser popularity 0.000145 description Wrap matching text in code span. cue code extends abstractMarkupParser string tag code string allAttributes class="scrollInlineCode" aftertextStrikeParser popularity 0.000048 description Wrap matching text in s span. cue strike extends abstractMarkupParser string tag s addClassMarkupParser popularity 0.000772 description Add a custom class to parent element. Provide query to add span matching text. extends abstractMarkupParser atoms cueAtom classNameAtom cue addClass string tag span javascript get applyToParentElement() { return this.atoms.length === 2 } getInserts(text) { // If no select text is added, set the class on the parent element. if (this.applyToParentElement) return [] return super.getInserts(text) } get className() { return this.getAtom(1) } get attributes() { return [`class="${this.className}"`] } get matchWholeLine() { return this.applyToParentElement } get pattern() { return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(" ") } hoverNoteParser popularity 0.000265 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. cueFromId extends addClassMarkupParser catchAllParser lineOfTextParser atoms cueAtom javascript get pattern() { return this.getAtomsFrom(1).join(" ") } get attributes() { return [`class="scrollHoverNote"`, `title="${this.hoverNoteText}"`] } get hoverNoteText() { return this.subparticlesToString().replace(/\n/g, " ") } scrollLinkParser popularity 0.008706 extends abstractMarkupParser description Put the matching text in an <a> tag. atoms cueAtom urlAtom inScope linkTitleParser linkTargetParser abstractCommentParser programParser description Anything here will be URI encoded and then appended to the link. cueFromId atoms cueAtom catchAllParser programLinkParser javascript get encoded() { return encodeURIComponent(this.subparticlesToString()) } cue link string tag a string linkAttribute href int patternStartsAtAtom 2 javascript buildTxt() { return this.root.ensureAbsoluteLink(this.link) + " " + this.pattern } get link() { const {baseLink} = this if (this.has("program")) return baseLink + this.getParticle("program").encoded return baseLink } get baseLink() { const link = this.getAtom(1) const isAbsoluteLink = link.includes("://") if (isAbsoluteLink) return link const relativePath = this.parent.buildSettings?.relativePath || "" return relativePath + link } get attributes() { const attrs = [`${this.linkAttribute}="${this.link}"`] const options = ["title", "target"] options.forEach(option => { const particle = this.getParticle(option) if (particle) attrs.push(`${option}="${particle.content}"`) }) return attrs } scrollClickParser extends scrollLinkParser description An a tag with an onclick. cue click string linkAttribute onclick emailLinkParser popularity 0.000048 description A mailto link cue email extends scrollLinkParser javascript get attributes() { return [`href="mailto:${this.link}"`] } quickLinkParser popularity 0.029228 pattern ^https?\: extends scrollLinkParser atoms urlAtom int patternStartsAtAtom 1 javascript get link() { return this.cue } quickRelativeLinkParser popularity 0.029228 description Relative links. // note: only works if relative link ends in .html pattern ^[^\s]+\.(html|htm) extends scrollLinkParser atoms urlAtom int patternStartsAtAtom 1 javascript get link() { return this.cue } datelineParser popularity 0.006005 cueFromId description Gives your paragraph a dateline like "December 15, 2021 ā The..." extends abstractAftertextDirectiveParser boolean matchWholeLine true javascript getInserts() { const {day} = this if (!day) return false return [{ index: 0, string: `<span class="scrollDateline">${day} ā </span>` }] } get day() { let day = this.content || this.root.date if (!day) return "" return this.root.dayjs(day).format(`MMMM D, YYYY`) } dayjsParser description Advanced directive that evals some Javascript code in an environment including "dayjs". cueFromId extends abstractAftertextDirectiveParser javascript getInserts() { const dayjs = this.root.dayjs const days = eval(this.content) const index = this.parent.originalTextPostLinkify.indexOf("days") return [{ index, string: `${days} ` }] } inlineMarkupsOnParser popularity 0.000024 cueFromId description Enable these inline markups only. example Hello *world*! inlineMarkupsOn bold extends abstractAftertextDirectiveParser catchAllAtomType inlineMarkupNameAtom boolean matchWholeLine true javascript get shouldMatchAll() { return true } get markups() { const {root} = this let markups = [{delimiter: "`", tag: "code", exclusive: true, name: "code", attributes: 'class="scrollInlineCode"'},{delimiter: "*", tag: "strong", name: "bold"}, {delimiter: "_", tag: "em", name: "italics"}] // only add katex markup if the root doc has katex. if (root.has("katex")) markups.unshift({delimiter: "$", tag: "span", attributes: ' class="scrollKatex"', exclusive: true, name: "katex"}) if (this.content) return markups.filter(markup => this.content.includes(markup.name)) if (root.has("inlineMarkups")) { root.getParticle("inlineMarkups").forEach(markup => { const delimiter = markup.getAtom(0) const tag = markup.getAtom(1) // todo: add support for providing custom functions for inline markups? // for example, !2+2! could run eval, or :about: could search a link map. const attributes = markup.getAtomsFrom(2).join(" ") markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups if (tag) markups.push({delimiter, tag, attributes}) }) } return markups } getMatches(text) { const exclusives = [] return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat() } applyMarkup(text, markup, exclusives = []) { const {delimiter, tag, attributes} = markup const escapedDelimiter = delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, "g") const delimiterLength = delimiter.length return [...text.matchAll(pattern)].map(match => { const { index } = match const endIndex = index + match[0].length // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now. // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style. // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done. if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1])) return undefined if (markup.exclusive) exclusives.push([index, endIndex]) return [ { index, string: `<${tag + (attributes ? " " + attributes : "")}>`, endIndex, consumeStartCharacters: delimiterLength }, { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength } ] }).filter(i => i) } inlineMarkupParser popularity 0.000169 cueFromId atoms cueAtom delimiterAtom tagOrUrlAtom catchAllAtomType htmlAttributesAtom extends inlineMarkupsOnParser description Custom inline markup. for example @This@ will be in italics. inlineMarkup @ em javascript getMatches(text) { try { const delimiter = this.getAtom(1) const tag = this.getAtom(2) const attributes = this.getAtomsFrom(3).join(" ") return this.applyMarkup(text, {delimiter, tag, attributes}) } catch (err) { console.error(err) return [] } // Note: doubling up doesn't work because of the consumption characters. } linkifyParser description Use this to disable linkify on the text. extends abstractAftertextDirectiveParser cueFromId atoms cueAtom booleanAtom abstractMarkupParameterParser atoms cueAtom cueFromId matchAllParser popularity 0.000024 description Use this to match all occurrences of the text. extends abstractMarkupParameterParser matchParser popularity 0.000048 catchAllAtomType integerAtom description Use this to specify which index(es) to match. javascript get indexes() { return this.getAtomsFrom(1).map(num => parseInt(num)) } example aftertext hello ello ello bold ello match 0 2 extends abstractMarkupParameterParser abstractHtmlAttributeParser linkTargetParser popularity 0.000024 extends abstractHtmlAttributeParser description If you want to set the target of the link. To "_blank", for example. cue target atoms cueAtom codeAtom blankLineParser popularity 0.308149 description Print nothing. Break section. atoms blankAtom boolean isPopular true javascript buildHtml() { return this.parent.clearSectionStack() } pattern ^$ tags doNotSynthesize scrollFileAddressParser catchAllAtomType filePathAtom catchAllParser scrollFileAddressParser chatLineParser popularity 0.009887 catchAllAtomType stringAtom catchAllParser chatLineParser lineOfCodeParser popularity 0.018665 catchAllAtomType codeAtom catchAllParser lineOfCodeParser commentLineParser catchAllAtomType commentAtom cssLineParser popularity 0.002870 catchAllAtomType cssAnyAtom catchAllParser cssLineParser abstractTableTransformParser atoms cueAtom inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser javascript get coreTable() { return this.parent.coreTable } get columnNames() { return this.parent.columnNames } connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) { const result = {} const normalize = str => str.toLowerCase().trim() userColumnNames.forEach(userColumn => { // Strategy 1: Exact match const exactMatch = availableColumnNames.find(col => col === userColumn) if (exactMatch) { result[userColumn] = exactMatch return } // Strategy 2: Case-insensitive match const normalizedUserColumn = normalize(userColumn) const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn) if (caseInsensitiveMatch) { result[userColumn] = caseInsensitiveMatch return } // Strategy 3: Levenshtein distance match const THRESHOLD = 2 // Consider matches with distance <= 2 as "very close" let bestMatch = null let bestDistance = Infinity availableColumnNames.forEach(col => { const distance = this.root.levenshteinDistance(userColumn, col) if (distance < bestDistance) { bestDistance = distance bestMatch = col } }) // Only use Levenshtein match if it's very close if (bestDistance <= THRESHOLD) { result[userColumn] = bestMatch return } // Strategy 4: Fallback - use original unmatched name result[userColumn] = userColumn }) return result } connectColumnName(name) { return this.connectColumnNames([name])[name] } getErrors() { if (this.silenceErrors) return [] // todo: generalize this? return super.getErrors() } getRunTimeEnumOptions(atom) { if (atom.atomTypeId === "columnNameAtom") return this.parent.columnNames return super.getRunTimeEnumOptions(atom) } getRunTimeEnumOptionsForValidation(atom) { // Note: this will fail if the CSV file hasnt been built yet. if (atom.atomTypeId === "columnNameAtom") return this.parent.columnNames.concat(this.parent.columnNames.map(c => "-" + c)) // Add reverse names return super.getRunTimeEnumOptions(atom) } abstractDateSplitTransformParser extends abstractTableTransformParser atoms cueAtom catchAllAtomType columnNameAtom javascript get coreTable() { const columnName = this.getAtom(1) || this.detectDateColumn() if (!columnName) return this.parent.coreTable return this.parent.coreTable.map(row => { const newRow = {...row} try { const date = this.root.dayjs(row[columnName]) if (date.isValid()) newRow[this.newColumnName] = this.transformDate(date) } catch (err) {} return newRow }) } detectDateColumn() { const columns = this.parent.columnNames const dateColumns = ['date', 'created', 'published', 'timestamp'] for (const col of dateColumns) { if (columns.includes(col)) return col } for (const col of columns) { const sample = this.parent.coreTable[0][col] if (sample && this.root.dayjs(sample).isValid()) return col } return null } get columnNames() { return [...this.parent.columnNames, this.newColumnName] } transformDate(date) { const formatted = date.format(this.dateFormat) const isInt = !this.cue.includes("Name") return isInt ? parseInt(formatted) : formatted } scrollSplitYearParser extends abstractDateSplitTransformParser description Extract year into new column. cue splitYear string newColumnName year string dateFormat YYYY scrollSplitDayNameParser extends abstractDateSplitTransformParser description Extract day name into new column. cue splitDayName string newColumnName dayName string dateFormat dddd scrollSplitMonthNameParser extends abstractDateSplitTransformParser description Extract month name into new column. cue splitMonthName string newColumnName monthName string dateFormat MMMM scrollSplitMonthParser extends abstractDateSplitTransformParser description Extract month number (1-12) into new column. cue splitMonth string newColumnName month string dateFormat M scrollSplitDayOfMonthParser extends abstractDateSplitTransformParser description Extract day of month (1-31) into new column. cue splitDayOfMonth string newColumnName dayOfMonth string dateFormat D scrollSplitDayOfWeekParser extends abstractDateSplitTransformParser description Extract day of week (0-6) into new column. cue splitDay string newColumnName day string dateFormat d scrollParseDateParser extends abstractTableTransformParser description Parse dates in a column into standard format. cue parseDate atoms cueAtom columnNameAtom example sampleData stocks.csv parseDate date linechart x date y price javascript get coreTable() { const columnName = this.connectColumnName(this.getAtom(1)) const formatOut = this.get("format") || "YYYY-MM-DD" const {dayjs} = this.root return this.parent.coreTable.map(row => { const newRow = {...row} try { const value = row[columnName] if (value) { const date = dayjs(value) if (date.isValid()) newRow[columnName] = date.format(formatOut) } } catch (err) { console.error(`Error parsing date in column ${columnName}:`, err) } return newRow }) } formatParser description Specify output date format atoms cueAtom stringAtom cueFromId single scrollGroupByParser catchAllAtomType columnNameAtom extends abstractTableTransformParser reduceParser description Specify how to aggregate a column when grouping data atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom cue reduce example data.csv groupBy year reduce score sum totalScore reduce name concat names printTable javascript get reduction() { return { source: this.getAtom(1), reduction: this.getAtom(2), name: this.getAtom(3) || this.getAtomsFrom(1).join("_") } } description Combine rows with matching values into groups. example tables posts.csv groupBy year printTable cue groupBy javascript get coreTable() { if (this._coreTable) return this._coreTable const groupByColNames = this.getAtomsFrom(1) const {coreTable} = this.parent if (!groupByColNames.length) return coreTable const newCols = this.findParticles("reduce").map(particle => particle.reduction) // Pivot is shorthand for group and reduce? const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => { const colMap = {} inputColumnNames.forEach((col) => (colMap[col] = true)) const groupByCols = groupByColumnNames.filter((col) => colMap[col]) return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols) } class PivotTable { constructor(rows, inputColumns, outputColumns) { this._columns = {} this._rows = rows inputColumns.forEach((col) => (this._columns[col.name] = col)) outputColumns.forEach((col) => (this._columns[col.name] = col)) } _getGroups(allRows, groupByColNames) { const rowsInGroups = new Map() allRows.forEach((row) => { const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, "") || "").join(" ") if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, []) rowsInGroups.get(groupKey).push(row) }) return rowsInGroups } getNewRows(groupByCols) { // make new particles const rowsInGroups = this._getGroups(this._rows, groupByCols) // Any column in the group should be reused by the children const columns = [ { name: "count", type: "number", min: 0, }, ] groupByCols.forEach((colName) => columns.push(this._columns[colName])) const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction) colsToReduce.forEach((col) => columns.push(col)) // for each group const rows = [] const totalGroups = rowsInGroups.size for (let [groupId, group] of rowsInGroups) { const firstRow = group[0] const newRow = {} groupByCols.forEach((col) => newRow[col] = firstRow ? firstRow[col] : 0 ) newRow.count = group.length // todo: add more reductions? count, stddev, median, variance. colsToReduce.forEach((col) => { const sourceColName = col.source const reduction = col.reduction const newColName = col.name if (reduction === "concat") { newRow[newColName] = group.map((row) => row[sourceColName]).join(" ") return } if (reduction === "first") { newRow[newColName] = group.find((row) => row[sourceColName] !== "")?.[sourceColName] return } const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === "number" && !isNaN(val)) let reducedValue = firstRow[sourceColName] if (reduction === "sum") reducedValue = values.reduce((prev, current) => prev + current, 0) if (reduction === "max") reducedValue = Math.max(...values) if (reduction === "min") reducedValue = Math.min(...values) if (reduction === "mean") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length newRow[newColName] = reducedValue }) rows.push(newRow) } // todo: add tests. figure out this api better. Object.values(columns).forEach((col) => { // For pivot columns, remove the source and reduction info for now. Treat things as immutable. delete col.source delete col.reduction }) return { rows, columns, } } } const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols) this._coreTable = pivotTable.rows this._columnNames = pivotTable.columns.map(col => col.name) return pivotTable.rows } get columnNames() { const {coreTable} = this return this._columnNames || this.parent.columnNames } scrollWhereParser extends abstractTableTransformParser description Filter rows by condition. cue where atoms cueAtom columnNameAtom comparisonAtom catchAllAtomType constantAtom example datatable iris.csv where Species = setosa javascript get coreTable() { // todo: use atoms here. const columnName = this.connectColumnName(this.getAtom(1)) const operator = this.getAtom(2) let comparisonSet if (operator === "oneOf") comparisonSet = new Set(this.atoms.slice(3)) let untypedComparisonValue = this.getAtom(3) const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue) const coreTable = this.parent.coreTable if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes("mpty"))) return coreTable const filterFn = row => { const atom = row[columnName] const typedAtom = atom === null ? undefined : atom // convert nulls to undefined if (operator === "=") return typedComparisonValue === typedAtom else if (operator === "!=") return typedComparisonValue !== typedAtom else if (operator === "includes") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue) else if (operator === "startsWith") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue) else if (operator === "endsWith") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue) else if (operator === "doesNotInclude") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue) else if (operator === "oneOf") return typedAtom !== undefined && comparisonSet.has(typedAtom.toString()) else if (operator === ">") return typedAtom > typedComparisonValue else if (operator === "<") return typedAtom < typedComparisonValue else if (operator === ">=") return typedAtom >= typedComparisonValue else if (operator === "<=") return typedAtom <= typedComparisonValue else if (operator === "empty") return typedAtom === "" || typedAtom === undefined else if (operator === "notEmpty") return typedAtom !== "" && typedAtom !== undefined } return coreTable.filter(filterFn) } scrollSelectParser catchAllAtomType columnNameAtom extends abstractTableTransformParser description Drop all columns except these. example tables data name,year,count index,2022,2 about,2023,4 select name year printTable cue select javascript get coreTable() { const {coreTable} = this.parent const {columnNames} = this if (!columnNames.length) return coreTable return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]]))) } get columnNames() { if (!this._columnNames) { const names = this.getAtomsFrom(1) this._columnNamesMap = this.connectColumnNames(names) this._columnNames = names.map(name => this._columnNamesMap[name]) } return this._columnNames } scrollReverseParser extends abstractTableTransformParser description Reverse rows. cue reverse javascript get coreTable() { return this.parent.coreTable.slice().reverse() } scrollComposeParser extends abstractTableTransformParser description Add column using format string. catchAllAtomType codeAtom cue compose atoms cueAtom newColumnNameAtom example datatable compose sentence My name is {name} printTable javascript get coreTable() { const {newColumnName} = this const formatString = this.getAtomsFrom(2).join(" ") return this.parent.coreTable.map((row, index) => { const newRow = Object.assign({}, row) newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index) return newRow }) } evaluate(str) { return str } get newColumnName() { return this.atoms[1] } get columnNames() { return this.parent.columnNames.concat(this.newColumnName) } scrollComputeParser extends scrollComposeParser description Add column by evaling format string. cue compute javascript evaluate(str) { return parseFloat(eval(str)) } scrollEvalParser extends scrollComputeParser description Add column by evaling format string. cue eval javascript evaluate(str) { return eval(str) } scrollRankParser extends scrollComposeParser description Add rank column. atoms cueAtom string newColumnName rank cue rank javascript evaluate(str, index) { return index + 1 } scrollLinksParser extends abstractTableTransformParser description Add column with links. cue links catchAllAtomType columnNameAtom string newColumnName links javascript get coreTable() { const {newColumnName, linkColumns} = this return this.parent.coreTable.map(row => { const newRow = Object.assign({}, row) let newValue = [] linkColumns.forEach(name => { const value = newRow[name] delete newRow[name] if (value) newValue.push(`<a href="${value.includes("@") ? "mailto:" : ""}${value}">${name}</a>`) }) newRow[newColumnName] = newValue.join(" ") return newRow }) } get linkColumns() { return this.getAtomsFrom(1) } get columnNames() { const {linkColumns} = this return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName) } scrollLimitParser extends abstractTableTransformParser description Select a subset. cue limit atoms cueAtom integerAtom integerAtom javascript get coreTable() { let start = this.getAtom(1) let end = this.getAtom(2) if (end === undefined) { end = start start = 0 } return this.parent.coreTable.slice(parseInt(start), parseInt(end)) } scrollShuffleParser extends abstractTableTransformParser description Randomly reorder rows. cue shuffle example datatable data.csv shuffle printTable javascript get coreTable() { // Create a copy of the datatable to avoid modifying original const rows = this.parent.coreTable.slice() // Fisher-Yates shuffle algorithm for (let i = rows.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[rows[i], rows[j]] = [rows[j], rows[i]] } return rows } scrollTransposeParser extends abstractTableTransformParser description Tranpose datatable. cue transpose javascript get coreTable() { // todo: we need to switch to column based coreTable, instead of row based const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]); return transpose(this.parent.coreTable) } scrollImputeParser extends abstractTableTransformParser description Impute missing values of a columm. atoms cueAtom columnNameAtom cue impute javascript get coreTable() { const {columnName} = this const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName) // ascending const imputed = [] let lastInserted = sorted[0][columnName] sorted.forEach(row => { const measuredTime = row[columnName] while (measuredTime > lastInserted + 1) { lastInserted++ // synthesize rows const imputedRow = {} imputedRow[columnName] = lastInserted imputedRow.count = 0 imputed.push(imputedRow) } lastInserted = measuredTime imputed.push(row) }) return imputed } get columnName() { return this.connectColumnName(this.getAtom(1)) } scrollOrderByParser extends abstractTableTransformParser description Sort rows by column(s). catchAllAtomType columnNameAtom cue orderBy javascript get coreTable() { const makeLodashOrderByParams = str => { const part1 = str.split(" ") const part2 = part1.map(col => (col.startsWith("-") ? "desc" : "asc")) return [part1.map(col => this.connectColumnName(col.replace(/^\-/, ""))), part2] } const orderBy = makeLodashOrderByParams(this.content) return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1]) } assertRowCountParser extends abstractTableTransformParser description Test row count is expected value. atoms cueAtom integerAtom cueFromId javascript getErrors() { const errors = super.getErrors() const actualRows = this.coreTable.length const expectedRows = parseInt(this.content) if (actualRows !== expectedRows) return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`)) return errors } scrollRenameParser // todo: add support in Parsers for tuple catch alls catchAllAtomType columnNameAtom newColumnNameAtom catchAllAtomType newColumnNameAtom extends abstractTableTransformParser description Rename columns. example tables data name,year,count index,2022,2 rename name Name year Year printTable cue rename javascript get coreTable() { const {coreTable} = this.parent const {renameMap} = this if (!Object.keys(renameMap).length) return coreTable return coreTable.map(row => { const newRow = {} Object.keys(row).forEach(key => { const name = renameMap[key] || key newRow[name] = row[key] }) return newRow }) } get renameMap() { const map = {} const pairs = this.getAtomsFrom(1) let oldName while (oldName = pairs.shift()) { map[oldName] = pairs.shift() } return map } _renamed get columnNames() { if (this._renamed) return this._renamed const {renameMap} = this this._renamed = this.parent.columnNames.map(name => renameMap[name] || name ) return this._renamed } scrollSummarizeParser extends abstractTableTransformParser description Generate summary statistics for each column. cue summarize example datatable data.csv summarize printTable javascript get coreTable() { const {lodash} = this.root const sourceData = this.parent.coreTable if (!sourceData.length) return [] return this.parent.columnNames.map(colName => { const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null) const numericValues = values.filter(val => typeof val === "number" && !isNaN(val)) const sorted = [...numericValues].sort((a, b) => a - b) // Calculate mode const frequency = {} values.forEach(val => { frequency[val] = (frequency[val] || 0) + 1 }) const mode = Object.entries(frequency) .sort((a, b) => b[1] - a[1]) .map(entry => entry[0])[0] // Calculate median for numeric values const median = sorted.length ? sorted.length % 2 === 0 ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2 : sorted[Math.floor(sorted.length/2)] : null const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null const theType = typeof values[0] const count = values.length const mean = theType === "number" ? sum/count : "" return { name: colName, type: theType, incompleteCount: sourceData.length - values.length, uniqueCount: new Set(values).size, count, sum, median, mean, min: sorted.length ? sorted[0] : null, max: sorted.length ? sorted[sorted.length - 1] : null, mode } }) } get columnNames() { return ["name", "type", "incompleteCount", "uniqueCount", "count", "sum", "median", "mean", "min", "max", "mode"] } errorParser baseParser errorParser heatrixCatchAllParser popularity 0.000193 // todo Fill this out catchAllAtomType stringAtom lineOfTextParser popularity 0.000289 catchAllAtomType stringAtom boolean isTextParser true htmlLineParser popularity 0.005209 catchAllAtomType htmlAnyAtom catchAllParser htmlLineParser openGraphParser // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser description Add this line to make this the open graph image. cueFromId atoms cueAtom importToFooterParser description Import to bottom of file. atoms parseTimeCommandAtom cue moveToFooter scriptLineParser catchAllAtomType javascriptAnyAtom catchAllParser scriptLineParser linkTitleParser popularity 0.000048 description If you want to set the title of the link. cue title atoms cueAtom catchAllAtomType stringAtom example * This report showed the treatment had a big impact. https://example.com/report This report. title The average growth in the treatment group was 14.2x higher than the control group. programLinkParser popularity 0.000531 catchAllAtomType codeAtom scrollMediaLoopParser popularity 0.000048 cue loop atoms cueAtom scrollAutoplayParser cue autoplay atoms cueAtom abstractCompilerRuleParser catchAllAtomType anyAtom atoms cueAtom closeSubparticlesParser extends abstractCompilerRuleParser description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank. cueFromId indentCharacterParser extends abstractCompilerRuleParser description You can change the indent character for compiled subparticles. Default is a space. cueFromId catchAllAtomDelimiterParser description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma. extends abstractCompilerRuleParser cueFromId openSubparticlesParser extends abstractCompilerRuleParser description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank. cueFromId stringTemplateParser extends abstractCompilerRuleParser description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId} cueFromId joinSubparticlesWithParser description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline. extends abstractCompilerRuleParser cueFromId abstractConstantParser description A constant. atoms cueAtom cueFromId // todo: make tags inherit tags actPhase parsersBooleanParser cue boolean atoms cueAtom constantIdentifierAtom catchAllAtomType booleanAtom extends abstractConstantParser tags actPhase parsersFloatParser cue float atoms cueAtom constantIdentifierAtom catchAllAtomType floatAtom extends abstractConstantParser tags actPhase parsersIntParser cue int atoms cueAtom constantIdentifierAtom catchAllAtomType integerAtom tags actPhase extends abstractConstantParser parsersStringParser cue string atoms cueAtom constantIdentifierAtom catchAllAtomType stringAtom catchAllParser catchAllMultilineStringConstantParser extends abstractConstantParser tags actPhase abstractParserRuleParser single atoms cueAtom abstractNonTerminalParserRuleParser extends abstractParserRuleParser parsersBaseParserParser atoms cueAtom baseParsersAtom description Set for blobs or errors. // 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. extends abstractParserRuleParser cue baseParser tags analyzePhase catchAllAtomTypeParser atoms cueAtom atomTypeIdAtom description Use for lists. // 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`. extends abstractParserRuleParser cueFromId tags analyzePhase atomParserParser atoms cueAtom atomParserAtom description Set parsing strategy. // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix. extends abstractParserRuleParser cueFromId tags experimental analyzePhase catchAllParserParser description Attach this to unmatched lines. // If a parser is not found in the inScope list, instantiate this type of particle instead. atoms cueAtom parserIdAtom extends abstractParserRuleParser cueFromId tags acquirePhase parsersAtomsParser catchAllAtomType atomTypeIdAtom description Set required atomTypes. extends abstractParserRuleParser cue atoms tags analyzePhase parsersCompilerParser // todo Remove this and its subparticles? description Deprecated. For simple compilers. inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser extends abstractParserRuleParser cue compiler tags deprecate boolean suggestInAutocomplete false parserDescriptionParser description Parser description. catchAllAtomType stringAtom extends abstractParserRuleParser cue description tags assemblePhase parsersExampleParser // todo Should this just be a "string" constant on particles? description Set example for docs and tests. catchAllAtomType exampleAnyAtom catchAllParser catchAllExampleLineParser extends abstractParserRuleParser cue example tags assemblePhase extendsParserParser cue extends tags assemblePhase description Extend another parser. // todo: add a catchall that is used for mixins atoms cueAtom parserIdAtom extends abstractParserRuleParser parsersPopularityParser // todo Remove this parser. Switch to conditional frequencies. description Parser popularity. atoms cueAtom floatAtom extends abstractParserRuleParser cue popularity tags assemblePhase inScopeParser description Parsers in scope. catchAllAtomType parserIdAtom extends abstractParserRuleParser cueFromId tags acquirePhase parsersJavascriptParser // todo Urgently need to get submode syntax highlighting running! (And eventually LSP) description Javascript code for Parser Actions. catchAllParser catchAllJavascriptCodeLineParser extends abstractParserRuleParser tags actPhase javascript format() { if (this.isNodeJs()) { const template = `class FOO{ ${this.subparticlesToString()}}` this.setSubparticles( require("prettier") .format(template, { semi: false, useTabs: true, parser: "babel", printWidth: 240 }) .replace(/class FOO \{\s+/, "") .replace(/\s+\}\s+$/, "") .replace(/\n\t/g, "\n") // drop one level of indent .replace(/\t/g, " ") // we used tabs instead of spaces to be able to dedent without breaking literals. ) } return this } cue javascript abstractParseRuleParser // Each particle should have a pattern that it matches on unless it's a catch all particle. extends abstractParserRuleParser cueFromId parsersCueParser atoms cueAtom stringAtom description Attach by matching first atom. extends abstractParseRuleParser tags acquirePhase cue cue cueFromIdParser atoms cueAtom description Derive cue from parserId. // for example 'fooParser' would have cue of 'foo'. extends abstractParseRuleParser tags acquirePhase parsersPatternParser catchAllAtomType regexAtom description Attach via regex. extends abstractParseRuleParser tags acquirePhase cue pattern parsersRequiredParser description Assert is present at least once. extends abstractParserRuleParser cue required tags analyzePhase abstractValidationRuleParser extends abstractParserRuleParser cueFromId catchAllAtomType booleanAtom parsersSingleParser description Assert used once. // Can be overridden by a child class by setting to false. extends abstractValidationRuleParser tags analyzePhase cue single uniqueLineParser description Assert unique lines. For pattern parsers. // Can be overridden by a child class by setting to false. extends abstractValidationRuleParser tags analyzePhase uniqueCueParser description Assert unique first atoms. For pattern parsers. // For catch all parsers or pattern particles, use this to indicate the extends abstractValidationRuleParser tags analyzePhase listDelimiterParser description Split content by this delimiter. extends abstractParserRuleParser cueFromId catchAllAtomType stringAtom tags analyzePhase contentKeyParser description Deprecated. For to/from JSON. // 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. extends abstractParserRuleParser cueFromId catchAllAtomType stringAtom tags deprecate boolean suggestInAutocomplete false subparticlesKeyParser // todo: deprecate? description Deprecated. For to/from JSON. // 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. extends abstractParserRuleParser cueFromId catchAllAtomType stringAtom tags deprecate boolean suggestInAutocomplete false parsersTagsParser catchAllAtomType stringAtom extends abstractParserRuleParser description Custom metadata. cue tags tags assemblePhase atomTypeDescriptionParser description Atom Type description. catchAllAtomType stringAtom cue description tags assemblePhase catchAllErrorParser baseParser errorParser catchAllExampleLineParser catchAllAtomType exampleAnyAtom catchAllParser catchAllExampleLineParser atoms exampleAnyAtom catchAllJavascriptCodeLineParser catchAllAtomType javascriptCodeAtom catchAllParser catchAllJavascriptCodeLineParser catchAllMultilineStringConstantParser description String constants can span multiple lines. catchAllAtomType stringAtom catchAllParser catchAllMultilineStringConstantParser atoms stringAtom atomTypeDefinitionParser // todo Generate a class for each atom type? // todo Allow abstract atom types? // todo Change pattern to postfix. pattern ^[a-zA-Z0-9_]+Atom$ inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser atoms atomTypeIdAtom tags assemblePhase javascript buildHtml() {return ""} async wake() { this.root.stackParserCode(this) } enumFromAtomTypesParser description Runtime enum options. catchAllAtomType atomTypeIdAtom atoms atomPropertyNameAtom cueFromId tags analyzePhase parsersEnumParser description Set enum options. cue enum catchAllAtomType enumOptionAtom atoms atomPropertyNameAtom tags analyzePhase parsersExamplesParser description Examples for documentation and tests. // 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. cue examples catchAllAtomType atomExampleAtom atoms atomPropertyNameAtom tags assemblePhase atomMinParser description Specify a min if numeric. cue min atoms atomPropertyNameAtom numberAtom tags analyzePhase atomMaxParser description Specify a max if numeric. cue max atoms atomPropertyNameAtom numberAtom tags analyzePhase parsersPaintParser atoms cueAtom paintTypeAtom description Instructor editor how to color these. single cue paint tags analyzePhase parserDefinitionParser // todo Add multiple dispatch? pattern ^[a-zA-Z0-9_]+Parser$ 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. catchAllParser catchAllErrorParser inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser atoms parserIdAtom tags assemblePhase javascript buildHtml() { return ""} async wake() { this.root.stackParserCode(this) } parsersRegexParser catchAllAtomType regexAtom description Atoms must match this. single atoms atomPropertyNameAtom cue regex tags analyzePhase reservedAtomsParser single description Atoms can't be any of these. catchAllAtomType reservedAtomAtom atoms atomPropertyNameAtom cueFromId tags analyzePhase extendsAtomTypeParser cue extends description Extend another atomType. // todo Add mixin support in addition to extends? atoms cueAtom atomTypeIdAtom tags assemblePhase single abstractColumnNameParser atoms cueAtom columnNameAtom javascript getRunTimeEnumOptions(atom) { if (atom.atomTypeId === "columnNameAtom") return this.parent.columnNames return super.getRunTimeEnumOptions(atom) } scrollRadiusParser cue radius extends abstractColumnNameParser scrollSymbolParser cue symbol extends abstractColumnNameParser abstractColumnNameOrColorParser extends abstractColumnNameParser atoms cueAtom columnNameOrColorAtom javascript //todo: cleanup colorOptions = "aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen".split(" ") getRunTimeEnumOptions(atom) { if (atom.atomTypeId === "columnNameOrColorAtom") return this.parent.columnNames.join(this.colorOptions) return super.getRunTimeEnumOptions(atom) } scrollFillParser cue fill extends abstractColumnNameOrColorParser scrollStrokeParser cue stroke extends abstractColumnNameOrColorParser scrollLabelParser cue label extends abstractColumnNameParser scrollSortParser cue sort extends abstractColumnNameParser scrollXParser cue x extends abstractColumnNameParser scrollYParser cue y extends abstractColumnNameParser abstractPlotLabelParser cueFromId atoms cueAtom catchAllAtomType stringAtom quoteLineParser popularity 0.004172 catchAllAtomType stringAtom catchAllParser quoteLineParser scrollParser description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas. root inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser catchAllParser catchAllParagraphParser javascript setFile(file) { this.file = file return this } buildHtml(buildSettings) { this.sectionStack = [] return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return ""} }).filter(i => i).join("\n") + this.clearSectionStack() } sectionStack = [] clearSectionStack() { const result = this.sectionStack.join("\n") this.sectionStack = [] return result } bodyStack = [] clearBodyStack() { const result = this.bodyStack.join("") this.bodyStack = [] return result } readSyncFromFileOrUrl(fileOrUrl) { if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl) const isUrl = fileOrUrl.match(/^https?\:[^ ]+$/) if (!isUrl) return this.root.readFile(fileOrUrl) return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop())) } getInBrowser(key) { return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : "") || "" } async fetch(url, filename) { const isUrl = url.match(/^https?\:[^ ]+$/) if (!isUrl) return return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url) } get path() { return require("path") } makeFullPath(filename) { return this.path.join(this.folderPath, filename) } _nextAndPrevious(arr, index) { const nextIndex = index + 1 const previousIndex = index - 1 return { previous: arr[previousIndex] ?? arr[arr.length - 1], next: arr[nextIndex] ?? arr[0] } } // keyboard nav is always in the same folder. does not currently support cross folder _includeFileInKeyboardNav(file) { const { scrollProgram } = file return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag) } _timestamp get timestamp() { if (!this._timestamp) { this._timestamp = this.file.timestamp const date = this.get("date") if (date) this._timestamp = this.dayjs(date).unix() } return this._timestamp } get linkToPrevious() { if (!this.hasKeyboardNav) // Dont provide link to next unless keyboard nav is on return undefined const {allScrollFiles} = this let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous if (!file) return undefined while (!this._includeFileInKeyboardNav(file)) { file = this._nextAndPrevious(allScrollFiles, file.scrollProgram.timeIndex).previous } return file.scrollProgram.permalink } get linkToNext() { if (!this.hasKeyboardNav) // Dont provide link to next unless keyboard nav is on return undefined const {allScrollFiles} = this let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next if (!file) return undefined while (!this._includeFileInKeyboardNav(file)) { file = this._nextAndPrevious(allScrollFiles, file.scrollProgram.timeIndex).next } return file.scrollProgram.permalink } // todo: clean up this naming pattern and add a parser instead of special casing 404.html get allHtmlFiles() { return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== "404.html") } parseNestedTag(tag) { if (!tag.includes("/")) return; const {path} = this const parts = tag.split("/") const group = parts.pop() const relativePath = parts.join("/") return { group, relativePath, folderPath: path.join(this.folderPath, path.normalize(relativePath)) } } getFilesByTags(tags, limit) { // todo: tags is currently matching partial substrings const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag)) if (typeof tags === "string") tags = tags.split(" ") if (!tags || !tags.length) return this.allHtmlFiles .filter(file => file !== this) // avoid infinite loops. todo: think this through better. .map(file => { return { file, relativePath: "" } }) .slice(0, limit) let arr = [] tags.forEach(tag => { if (!tag.includes("/")) return (arr = arr.concat( getFilesWithTag(tag, this.allScrollFiles) .map(file => { return { file, relativePath: "" } }) .slice(0, limit) )) const {folderPath, group, relativePath} = this.parseNestedTag(tag) let files = [] try { files = this.fileSystem.getFusedFilesInFolderIfCached(folderPath, this) } catch (err) { console.error(err) } const filtered = getFilesWithTag(group, files).map(file => { return { file, relativePath: relativePath + "/" } }) arr = arr.concat(filtered.slice(0, limit)) }) return this.lodash.sortBy(arr, file => file.file.scrollProgram.timestamp).reverse() } async fetchNode(url, filename) { filename = filename || new URL(url).pathname.split('/').pop() const fullpath = this.makeFullPath(filename) if (require("fs").existsSync(fullpath)) return this.readFile(fullpath) this.log(`š fetching ${url} to ${fullpath} `) try { await this.downloadToDisk(url, fullpath) return this.readFile(fullpath) } catch (err) { console.error(`fetching ${url} failed`) console.error(err) return "" } } log(message) { if (this.logger) this.logger.log(message) } async fetchBrowser(url) { const content = this.getInBrowser(url) if (content) return content return this.downloadToLocalStorage(url) } async downloadToDisk(url, destination) { const { writeFile } = require('fs').promises const response = await fetch(url) const fileBuffer = await response.arrayBuffer() await writeFile(destination, Buffer.from(fileBuffer)) return this.readFile(destination) } async downloadToLocalStorage(url) { const response = await fetch(url) const blob = await response.blob() const text = await blob.text() try { localStorage.setItem(url, text) return localStorage.getItem(url) } catch (err) { if (!window.inMemStorage) window.inMemStorage = {} window.inMemStorage[url] = text console.error(err) return text } } readFile(filename) { const {path} = this const fs = require("fs") const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, "")) try { if (fs.existsSync(fullPath)) return fs.readFileSync(fullPath, "utf8") console.error(`File '${filename}' not found`) return "" } catch (err) { console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`) console.error(err) return "" } } alreadyRequired = new Set() buildHtmlSnippet(buildSettings) { this.sectionStack = [] return this.map(subparticle => { try { if (subparticle.noSnippet) return "" return subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : (subparticle.buildHtml ? subparticle.buildHtml(buildSettings) : "")} catch (err) { console.error(`Error building snippet in file '${this.filename}' in particle '${subparticle.toString()}'`) console.error(err) return "" } }) .filter(i => i) .join("\n") .trim() + this.clearSectionStack() } get footnotes() { if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote) return this._footnotes } get authors() { return this.get("authors") } _addTimeIndexes(files) { // A little hacky but ah well. // For blogs, we want to sort posts in a folder in order of the date property. // If the date property is not set, we use the file timestamp from disk. const sorted = files.sort((a, b) => b.scrollProgram.timestamp - a.scrollProgram.timestamp) sorted.forEach((file, index) => (file.scrollProgram.timeIndex = index)) } get allScrollFiles() { try { const files = this.fileSystem.getFusedFilesInFolderIfCached(this.folderPath, this) if (this.timeIndex === undefined) this._addTimeIndexes(files) return files } catch (err) { console.error(err) return [] } } async doThing(thing) { await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]())) } async load() { // todo: document when to use load instead of wake? do we need both? await this.doThing("load") } async execute() { await this.doThing("execute") } file = {} getFromParserId(parserId) { return this.parserIdIndex[parserId]?.[0].content } notifyOnReady(particle) { if (!this._toNotify) this._toNotify = [] this._toNotify.push(particle) } wake() { // In case the file ended on a parser definition, refresh parser pool this._refreshParserPool() if (this._toNotify) this._toNotify.forEach(particle => particle.onReady()) } get fileSystem() { return this.file.fileSystem } get filePath() { // Note: we currently do not support files with spaces in their filepaths return this.cue } get folderPath() { return Utils.posix.dirname(this.filePath) + "/" } get filename() { return Utils.posix.basename(this.filePath) } get hasKeyboardNav() { return this.has("keyboardNav") } get editHtml() { return `<a href="${this.editUrl}" class="abstractTextLinkParser">Edit</a>` } get externalsPath() { return this.path.join(this.fileSystem.standardParserDirectory, "..", "external") } get endSnippetIndex() { // Get the line number that the snippet should stop at. // First if its hard coded, use that if (this.has("endSnippet")) return this.getParticle("endSnippet").index // Next look for a dinkus const snippetBreak = this.find(particle => particle.isDinkus) if (snippetBreak) return snippetBreak.index return -1 } get parserIds() { return this.topDownArray.map(particle => particle.definition.id) } get tags() { return this.get("tags") || "" } get primaryTag() { return this.tags.split(" ")[0] } get filenameNoExtension() { return this.filename.replace(".scroll", "") } // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost) // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work get baseUrl() { const baseUrl = (this.get("baseUrl") || "").replace(/\/$/, "") return baseUrl + "/" } get canonicalUrl() { return this.get("canonicalUrl") || this.baseUrl + this.permalink } get openGraphImage() { const openGraphImage = this.get("openGraphImage") if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage) const images = this.filter(particle => particle.doesExtend("scrollImageParser")) const hit = images.find(particle => particle.has("openGraph")) || images[0] if (!hit) return "" return this.ensureAbsoluteLink(hit.filename) } get absoluteLink() { return this.ensureAbsoluteLink(this.permalink) } ensureAbsoluteLink(link) { if (link.includes("://")) return link return this.baseUrl + link.replace(/^\//, "") } get editUrl() { const editUrl = this.get("editUrl") if (editUrl) return editUrl const editBaseUrl = this.get("editBaseUrl") return (editBaseUrl ? editBaseUrl.replace(/\/$/, "") + "/" : "") + this.filename } get gitRepo() { // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll // return https://github.com/breck7/breckyunits.com return this.editUrl.split("/").slice(0, 5).join("/") } get scrollVersion() { // currently manually updated return "177.0.1" } // Use the first paragraph for the description // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?) // would speed up a lot. get description() { const description = this.getFromParserId("openGraphDescriptionParser") if (description) return description return this.generatedDescription } get keywords() { if (this.has("keywords")) return this.get("keywords") const tags = this.get("tags") if (tags) return tags.split(" ").join(", ") return "" } get generatedDescription() { const firstParagraph = this.find(particle => particle.isArticleContent) return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&"<>']/g, "") : "" } get titleFromFilename() { const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, match => match.toUpperCase()) return unCamelCase(this.filenameNoExtension) } get title() { return this.getFromParserId("scrollTitleParser") || this.titleFromFilename } get linkTitle() { return this.getFromParserId("scrollLinkTitleParser") || this.title } get permalink() { return this.get("permalink") || (this.filename ? this.filenameNoExtension + ".html" : "") } compileTo(extensionCapitalized) { if (extensionCapitalized === "Txt") return this.asTxt if (extensionCapitalized === "Html") return this.asHtml const methodName = "build" + extensionCapitalized return this.topDownArray .filter(particle => particle[methodName]) .map((particle, index) => particle[methodName](index)) .join("\n") .trim() } get asTxt() { return ( this.map(particle => { const text = particle.buildTxt ? particle.buildTxt() : "" if (text) return text + "\n" if (!particle.getLine().length) return "\n" return "" }) .join("") .replace(/\n\n\n+/g, "\n\n") // Maximum 2 newlines in a row .trim() + "\n" // Always end in a newline, Posix style ) } get dependencies() { const dependencies = this.file.dependencies?.slice() || [] const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat() return dependencies.concat(files) } get importOnly() { return this.has("importOnly") } get buildsHtml() { const { permalink } = this return !this.importOnly && (permalink.endsWith(".html") || permalink.endsWith(".htm")) } // Without specifying the language hyphenation will not work. get lang() { return this.get("htmlLang") || "en" } _compiledHtml = "" get asHtml() { if (!this._compiledHtml) { const { permalink, buildsHtml } = this const content = (this.buildHtml() + this.clearBodyStack()).trim() // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But // <1% of use case so might be good enough. const wrapWithHtmlTags = buildsHtml const bodyTag = this.has("metaTags") ? "" : "<body>\n" this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\n<html lang="${this.lang}">\n${bodyTag}${content}\n</body>\n</html>` : content } return this._compiledHtml } get wordCount() { return this.asTxt.match(/\b\w+\b/g)?.length || 0 } get minutes() { return parseFloat((this.wordCount / 200).toFixed(1)) } get date() { return this.dateObject.format(`MM/DD/YYYY`) } get dateObject() { const date = this.get("date") || (this.timestamp || 0) return this.dayjs(date) } get year() { return parseInt(this.dayjs(this.date).format(`YYYY`)) } get dayjs() { if (!this.isNodeJs()) return dayjs const lib = require("dayjs") const relativeTime = require("dayjs/plugin/relativeTime") lib.extend(relativeTime) return lib } get lodash() { return this.isNodeJs() ? require("lodash") : lodash } get d3() { return this.isNodeJs() ? require('d3') : d3 } getConcepts(parsed) { const concepts = [] let currentConcept parsed.forEach(particle => { if (particle.isConceptDelimiter) { if (currentConcept) concepts.push(currentConcept) currentConcept = [] } if (currentConcept && particle.isMeasure) currentConcept.push(particle) }) if (currentConcept) concepts.push(currentConcept) return concepts } _formatConcepts(parsed) { const concepts = this.getConcepts(parsed) if (!concepts.length) return false const {lodash} = this // does a destructive sort in place on the parsed program concepts.forEach(concept => { let currentSection const newCode = lodash .sortBy(concept, ["sortIndex"]) .map(particle => { let newLines = "" const section = particle.sortIndex.toString().split(".")[0] if (section !== currentSection) { currentSection = section newLines = "\n" } return newLines + particle.toString() }) .join("\n") concept.forEach((particle, index) => (index ? particle.destroy() : "")) concept[0].replaceWith(newCode) }) } get formatted() { return this.getFormatted(this.codeAtStart) } get lastCommitTime() { // todo: speed this up and do a proper release. also could add more metrics like this. if (this._lastCommitTime === undefined) { try { this._lastCommitTime = require("child_process").execSync(`git log -1 --format="%at" -- "${this.filePath}"`).toString().trim() } catch (err) { this._lastCommitTime = 0 } } return this._lastCommitTime } get codeAtStart() { return this.file.codeAtStart } async ensureFileLoaded() { // todo: cleanup if (!this.codeAtStart) await this.file._readCodeFromStorage() } getFormatted(codeAtStart = this.toString()) { let formatted = codeAtStart.replace(/\r/g, "") // remove all carriage returns if there are any const parsed = new this.latestConstructor(formatted) parsed.topDownArray.forEach(subparticle => { subparticle.format() const original = subparticle.getLine() const trimmed = original.replace(/(\S.*?)[ \t]*$/gm, "$1") // Trim trailing whitespace unless parser allows it if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed) }) this._formatConcepts(parsed) let importOnlys = [] let topMatter = [] let allElse = [] // Create any bindings parsed.forEach(particle => { if (particle.bindTo === "next" && !particle.isLast) particle.binding = particle.next if (particle.bindTo === "previous" && !particle.isFirst) particle.binding = particle.previous }) let noFormat = false parsed.forEach(particle => { if (noFormat) return allElse.push(particle) if (particle.getLine() === "noFormat") { noFormat = true return allElse.push(particle) } if (particle.getLine() === "importOnly") importOnlys.push(particle) else if (particle.isTopMatter) topMatter.push(particle) else allElse.push(particle) }) const combined = importOnlys.concat(topMatter, allElse) // Move any bound particles combined .filter(particle => particle.binding) .forEach(particle => { // First remove the particle from its current position const originalIndex = combined.indexOf(particle) combined.splice(originalIndex, 1) // Then insert it at the new position // We need to find the binding index again after removal const bindingIndex = combined.indexOf(particle.binding) if (particle.bindTo === "next") combined.splice(bindingIndex, 0, particle) else combined.splice(bindingIndex + 1, 0, particle) }) const trimmed = combined .map(particle => particle.toString()) .join("\n") .replace(/^\n*/, "") // Remove leading newlines .replace(/\n\n\n+/g, "\n\n") // Maximum 2 newlines in a row .replace(/\n+$/, "") return trimmed === "" ? trimmed : trimmed + "\n" // End non blank Scroll files in a newline character POSIX style for better working with tools like git } get parser() { return this.constructor } get parsersRequiringExternals() { const { parser } = this // todo: could be cleaned up a bit if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0]) return parser.parsersRequiringExternals } get Disk() { return this.isNodeJs() ? require("scrollsdk/products/Disk.node.js").Disk : {}} async buildAll(options = {}) { await this.load() await this.buildOne(options) await this.buildTwo(options) } async buildOne(options) { await this.execute() const toBuild = this.filter(particle => particle.buildOne) for (let particle of toBuild) { await particle.buildOne(options) } } async buildTwo(options) { const toBuild = this.filter(particle => particle.buildTwo) for (let particle of toBuild) { await particle.buildTwo(options) } } stackParserCode(parser) { // Add top level parsers only which will add their nested parsers. if (parser.parent !== this) return // Perf hack until we can refactor ParserPool and add incremental addition of parsers. // What we see in the wild is that frequently you'll have parser definitions one after // the other, separated by blank lines or comments. So we stack up those parsers and // dont add them until we are done with that section. if (this._parsersToRegister === undefined) this._parsersToRegister = [] this._parsersToRegister.push(parser) if (!this._beforeAppend) this._beforeAppend = (block) => { const line1 = block.split(/\n/)[0] if (line1 === "" || line1.endsWith("Parser") || line1.startsWith("//")) return this._refreshParserPool() } } _refreshParserPool() { if (!this._parsersToRegister) return this.registerParsers(this._parsersToRegister.map(p => p.toString()).join("\n"), this.filePath) delete this._beforeAppend delete this._parsersToRegister } get outputFileNames() { return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat() } _compileArray(filename, arr) { const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== ""))) const parts = filename.split(".") const format = parts.pop() if (format === "json") return JSON.stringify(removeBlanks(arr), null, 2) if (format === "js") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2) if (format === "csv") return this.arrayToCSV(arr) if (format === "tsv") return this.arrayToCSV(arr, "\t") if (format === "particles") return particles.toString() return particles.toString() } inspect(particles = this.getSubparticles()) { const mapFn = particle => { const atomTypes = particle.lineAtomTypes.split(" ") return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(" ")}${(particle.length ? `\n ` + particle.map(mapFn).join("\n") + `` : "")}`} return particles.map(mapFn).join("\n") } levenshteinDistance(a, b) { const m = a.length const n = b.length const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)) for (let i = 0; i <= m; i++) { dp[i][0] = i } for (let j = 0; j <= n; j++) { dp[0][j] = j } for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1 dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost) } } return dp[m][n] } makeLodashOrderByParams(str) { const part1 = str.split(" ") const part2 = part1.map(col => (col.startsWith("-") ? "desc" : "asc")) return [part1.map(col => col.replace(/^\-/, "")), part2] } arrayToCSV(data, delimiter = ",") { if (!data.length) return "" // Extract headers const headers = Object.keys(data[0]) const csv = data.map(row => headers .map(fieldName => { const fieldValue = row[fieldName] // Escape commas if the value is a string if (typeof fieldValue === "string" && (fieldValue.includes(delimiter) || fieldValue.includes('"'))) { return `"${fieldValue.replace(/"/g, '""')}"` // Escape double quotes and wrap in double quotes } return fieldValue }) .join(delimiter) ) csv.unshift(headers.join(delimiter)) // Add header row at the top return csv.join("\n") } compileConcepts(filename = "csv", sortBy = "") { const {lodash} = this if (!sortBy) return this._compileArray(filename, this.concepts) const orderBy = this.makeLodashOrderByParams(sortBy) return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1])) } _withStats get measuresWithStats() { if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures) return this._withStats } addMeasureStats(concepts, measures){ return measures.map(measure => { let Type = false concepts.forEach(concept => { const value = concept[measure.Name] if (value === undefined || value === "") return measure.Values++ if (!Type) { measure.Example = value.toString().replace(/\n/g, " ") measure.Type = typeof value Type = true } }) measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + "%" return measure }) } get latestConstructor() { // todo: cleanup return this._modifiedConstructor || this.constructor } _parseMeasures() { const {lodash, definition} = this // cache measures on the _Parser_, since they are unique to a parser. if (definition._measures) return definition._measures // todo: clean this up // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers const dummyCode = Array.from(new Set(definition.map(particle => particle.get("cue") || particle.getLine().replace(/Parser$/, "")))).join("\n") const parser = this.latestConstructor const dummyProgram = new parser(dummyCode, this.getLine()) // Delete any particles that are not measures dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy()) dummyProgram.forEach(particle => { // add nested measures Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key)) }) // Delete any nested particles that are not measures dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy()) const measures = dummyProgram.topDownArray.map(particle => { return { Name: particle.measureName, Values: 0, Coverage: 0, Question: particle.definition.description, Example: particle.definition.getParticle("example")?.subparticlesToString() || "", Type: particle.typeForWebForms, Source: particle.sourceDomain, //Definition: parsedProgram.root.filename + ":" + particle.lineNumber SortIndex: particle.sortIndex, IsComputed: particle.isComputed, IsRequired: particle.isMeasureRequired, IsConceptDelimiter: particle.isConceptDelimiter, Cue: particle.definition.get("cue") } }) definition._measures = lodash.sortBy(measures, "SortIndex") return definition._measures } _concepts get concepts() { if (!this._concepts) this._concepts = this._parseConcepts() return this._concepts } get concept() { return this.concepts[0] } _measures get measures() { if (!this._measures) return this._measures = this._parseMeasures() return this._measures } _parseConcepts(){ const parsedProgram = this const measures = this.measures // Todo: might be a perf/memory/simplicity win to have a "segment" method in ScrollSDK, where you could // virtually split a Particle into multiple segments, and then query on those segments. // So we would "segment" on "id ", and then not need to create a bunch of new objects, and the original // already parsed lines could then learn about/access to their respective segments. const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0] if (!conceptDelimiter) return [] const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name) concepts.shift() // Remove the part before "id" return concepts.map(concept => { const row = {} measures.forEach(measure => { const measureName = measure.Name const measureKey = measure.Cue || measureName.replace(/_/g, " ") if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? "" else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts) }) return row }) } computeMeasure(parsedProgram, measureName, concept, concepts){ // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll if (!Particle.measureFnCache) Particle.measureFnCache = {} const measureFnCache = Particle.measureFnCache if (!measureFnCache[measureName]) { // a bit hacky but works?? const particle = parsedProgram.appendLine(measureName) measureFnCache[measureName] = particle.computeValue particle.destroy() } return measureFnCache[measureName](concept, measureName, parsedProgram, concepts) } compileMeasures(filename = "csv", sortBy = "") { const withStats = this.measuresWithStats if (!sortBy) return this._compileArray(filename, withStats) const orderBy = this.makeLodashOrderByParams(sortBy) return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1])) } evalNodeJs(particle) { if (!this.isNodeJs()) return false const {filePath, Disk} = this const uid = particle._getUid() const tempPath = filePath + `.${uid}.js` if (Disk.exists(tempPath)) throw new Error(`Failed to write/require nodejs snippet since '${tempPath}' already exists.`) try { Disk.write(tempPath, particle.subparticlesToString()) return require(tempPath) } catch (err) { console.error(`Error in ${particle.cue} in file '${filePath}'`) console.error(err) return false } finally { Disk.rm(tempPath) } } toRss() { const { title, canonicalUrl } = this return ` <item> <title>${title}</title> <link>${canonicalUrl}</link> <pubDate>${this.dayjs(this.timestamp * 1000).format("ddd, DD MMM YYYY HH:mm:ss ZZ")}</pubDate> </item>` } example # Hello world ## This is Scroll * It compiles to HTML. code // You can add code as well. print("Hello world") stampFileParser catchAllAtomType stringAtom description Create a file. javascript execute(parentDir) { const fs = require("fs") const path = require("path") const fullPath = path.join(parentDir, this.getLine()) this.root.log(`Creating file ${fullPath}`) fs.mkdirSync(path.dirname(fullPath), {recursive: true}) const content = this.subparticlesToString() fs.writeFileSync(fullPath, content, "utf8") const isExecutable = content.startsWith("#!") if (isExecutable) fs.chmodSync(fullPath, "755") } stampFolderParser catchAllAtomType stringAtom description Create a folder. inScope stampFolderParser catchAllParser stampFileParser pattern \/$ javascript execute(parentDir) { const fs = require("fs") const path = require("path") const newPath = path.join(parentDir, this.getLine()) this.root.log(`Creating folder ${newPath}`) fs.mkdirSync(newPath, {recursive: true}) this.forEach(particle => particle.execute(newPath)) } abstractTableOptionsParser atoms cueAtom stringAtom scrollJsonPathParser popularity 0.001037 description Specify dot path to data. cue path extends abstractTableOptionsParser scrollTableDelimiterParser popularity 0.001037 description Set the delimiter. cue delimiter extends abstractTableOptionsParser scrollTableDataParser popularity 0.001061 cue data atoms cueAtom description Table from inline delimited data. baseParser blobParser plainTextLineParser popularity 0.000121 catchAllAtomType stringAtom catchAllParser plainTextLineParser BlobParser baseParser blobParser ``` |
04/14/2025 |
29530 |
147.7 |
Edit |
Edit |
04/14/2025 |
1 |
0 |
Empty |
|
04/14/2025 |
0 |
0 |
Example |
This was a nasty little bug. Basically when using macros if the replacement string contained $&, and was nested, then the replacement would contain the match, and then since it was nested we'd encounter it again, leading to max call stack exceeded. |
04/14/2025 |
42 |
0.2 |
Favicon |
|
04/14/2025 |
0 |
0 |
Feed |
<?xml version="1.0" encoding="ISO-8859-1" ?> <rss version="2.0"> <channel> <title>Feed</title> <link>https://scroll.pub/tests/</link> <description>Kitchen sink test site.</description> <lastBuildDate>Mon, 14 Apr 2025 13:01:02 +0000</lastBuildDate> <language>en-us</language> <item> <title>Tabular data test</title> <link>https://scroll.pub/tests/tabularData.html</link> <pubDate>Wed, 21 Apr 57255 02:42:45 +0000</pubDate> </item> <item> <title>A review of my sink</title> <link>https://scroll.pub/tests/a-review-of-my-sink.html</link> <pubDate>Wed, 21 Apr 57255 02:42:41 +0000</pubDate> </item> <item> <title>Top Sinks</title> <link>https://scroll.pub/tests/top-sinks.html</link> <pubDate>Fri, 11 Jan 2019 00:00:00 +0000</pubDate> </item> </channel> </rss> |
04/14/2025 |
125 |
0.6 |
Fetch Test |
|
04/14/2025 |
0 |
0 |
Footer |
Built with Scroll v177.0.1 |
04/14/2025 |
6 |
0 |
Gazette |
Gazette ======= A header ======== A link. https://scroll.pub link - A list - Item 2 Code ==== Inline `code`. ```header A code block ``` |
04/14/2025 |
20 |
0.1 |
Haml |
Green Red Red underline Green underline plain tag - About - Home |
04/14/2025 |
10 |
0.1 |
Heatrix |
Basic test ========== Multiple rows ============= Test Links ========== Test Size and Number escaping and custom colors =============================================== |
04/14/2025 |
14 |
0.1 |
Html |
The background should be green |
04/14/2025 |
5 |
0 |
A review of my sink |
A review of my sink =================== I have been using my sink for one year, and this is what I have learned. I have learned that a sink is a sink. My favorite things about sinks ============================== - They can hold water - You can put things in them - This should be in a new list - days old - foo https://scroll.pub/tests/bar.html foo - bar https://scroll.pub/tests/bam.html bar |
04/14/2025 |
70 |
0.3 |
A Parser |
The below should just be parsed as text: hiddenMessage Click me. message Hello world The below should be parsed by the parser above Click me. message Hello world |
04/14/2025 |
28 |
0.1 |
About the Kitchen Sink Blog |
About the Kitchen Sink Blog =========================== This blog is for testing purposesāto exercise/test all the features of Scroll. April 14, 2025 Public domain products are strictly superior to equivalent non-public domain alternatives by a significant margin on three dimensions: trust, speed, and cost to build. If enough capable people start building public domain products we can change the world. |
04/14/2025 |
61 |
0.3 |
Atom Types |
|
04/14/2025 |
0 |
0 |
Authors |
Authors should not print anything. |
04/14/2025 |
5 |
0 |
Auto Title |
Auto Title ========== April 14, 2025 This file has no title set and no date set, but those should be autogenerated from the filename and file ctime data. |
04/14/2025 |
27 |
0.1 |
Background |
hello world |
04/14/2025 |
2 |
0 |
Blog Post |
Blog Post ========= This is my body. This should be above the footer. Built with Scroll v177.0.1 |
04/14/2025 |
18 |
0.1 |
Top Sinks |
Top Sinks ========= by Breck Yunits https://breckyunits.com Breck Yunits 283 in database $0 budget 3 countries sinkManufacturer|country Blue|USA Red|Canada sinkManufacturer|country Blue|USA Red|Canada sinkManufacturer|country Blue|USA Red|Canada Today is Mon Apr 14 2025 13:01:00 GMT+0000 (Coordinated Universal Time) 1+1 = 2 January 11, 2019 was a great day There are 283 in my list. This file is in dir: /home/runner/work/scroll/scroll/tests |
01/11/2019 |
76 |
0.4 |