├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── bin └── trio.js ├── index.js ├── lib ├── buildanalyzer │ ├── fragmentFrontMatterCheck │ │ └── index.js │ ├── index.js │ ├── integrityCheck │ │ └── index.js │ └── resolveDependencies │ │ ├── groupStatsByCategory.js │ │ ├── index.js │ │ └── resolvers │ │ ├── getAllIncludeDepsFromResolvedFragment.js │ │ ├── getCallbackDeps.js │ │ ├── getDataDeps.js │ │ ├── getFilterDeps.js │ │ ├── getIncludeDeps.js │ │ ├── getRequiredModuleDeps.js │ │ ├── getTemplateDeps.js │ │ └── index.js ├── collections │ ├── filter.js │ ├── fragmentFactory.js │ ├── index.js │ ├── validateDataset.js │ └── validateItem.js ├── config │ ├── defaultOptions.js │ ├── fileNames.js │ ├── index.js │ ├── permalinks.js │ ├── userConfig.js │ └── validatePermalinks.js ├── generator │ ├── blog.js │ ├── cleanPublic.js │ ├── copyOtherFiles.js │ ├── createManifest.js │ ├── garbageCollector.js │ ├── getAssets.js │ ├── index.js │ ├── makePublicFolder.js │ ├── mashup.js │ ├── prefixUrls.js │ ├── prepareForReleaseBuild.js │ ├── removeComments.js │ ├── removeDataAttributes.js │ ├── reportMissingAssets.js │ ├── reportWips.js │ ├── sassRender.js │ └── save.js ├── metadata │ ├── createRelatedArticlesByCategory.js │ ├── createRelatedArticlesByTag.js │ ├── createRelatedArticlesByTagFlattened.js │ ├── createSortedArticlesCatalog.js │ ├── createSortedCategoriesCatalog.js │ ├── createSortedTagCatalog.js │ ├── getAllData.js │ ├── getAllIncludesMetadata.js │ ├── getArticleDateFromPath.js │ ├── getFragmentMetadata.js │ ├── getPublicDestPath.js │ └── index.js ├── metrics │ └── index.js ├── stats │ └── getStats.js ├── tasks │ ├── cache-bust │ │ └── cacheBust.js │ ├── create-new-project │ │ └── index.js │ └── file-watcher │ │ └── index.js └── utils │ ├── allArayElsToLowerCase.js │ ├── articlesHaveCategories.js │ ├── articlesHaveTags.js │ ├── browserSync.js │ ├── callTagCallbacks.js │ ├── createRelatedItem.js │ ├── dateValidator.js │ ├── doesCachedStatsFileExists.js │ ├── getAllIncludes.js │ ├── getCachedStatsFile.js │ ├── getFileModifiedTime.js │ ├── getIgnoredSourceFolders.js │ ├── getTagCallbackDependencies.js │ ├── getTagCallbacks.js │ ├── getTemplateFileName.js │ ├── globFriendly.js │ ├── index.js │ ├── isFile.js │ ├── isPlural.js │ ├── log.js │ ├── matterCache.js │ ├── mdToHtml.js │ ├── metadataHasArticles.js │ ├── metadataHasPath.js │ ├── normalizeFileName.js │ ├── publicPathToUrl.js │ ├── readCache.js │ ├── resolveIndirectPathToInclude.js │ ├── showDeprecated.js │ ├── sortArticles.js │ ├── toArray.js │ ├── triggerOneOffBuild.js │ ├── warn.js │ ├── writeCachedMetaFile.js │ └── writeCachedStatsFile.js ├── npm-shrinkwrap.json ├── package.json └── scaffolding └── reference-project-1 ├── .gitignore ├── source ├── callbacks │ ├── archivePage.js │ ├── articleNavigator.js │ ├── articlePage.js │ ├── blogNavigator.js │ ├── blogPage.js │ ├── categoriesList.js │ ├── categoryPage.js │ ├── footer.js │ ├── getBrand.js │ ├── header.js │ ├── relatedArticlesList.js │ ├── setBlogPageDocTitle.js │ ├── setCategoryPageDocTitle.js │ ├── setCategoryPageTitle.js │ └── setDocTitle.js ├── data │ ├── brand.json │ └── social.json ├── filters │ ├── archivePageArticlesFilter.js │ ├── blogPagesArticlesFilter.js │ └── categoryPagesArticlesFilter.js ├── fragments │ ├── about.md │ ├── articles │ │ ├── 2019-01-01-article1.md │ │ ├── 2019-02-01-article2.md │ │ ├── 2019-03-01-article3.md │ │ ├── 2019-04-16-article4.md │ │ ├── 2019-05-08-article5.md │ │ └── 2019-06-30-article6.md │ ├── blog │ │ ├── archive │ │ │ └── index.md │ │ ├── category │ │ │ └── category.html │ │ └── index.md │ └── index.md ├── includes │ ├── footer.html │ └── header.html ├── sass │ ├── _archivelink.scss │ ├── _archivepage.scss │ ├── _articleimage.scss │ ├── _articlepage.scss │ ├── _banner.scss │ ├── _blogpage.scss │ ├── _categorieslist.scss │ ├── _categorypage.scss │ ├── _container.scss │ ├── _footer.scss │ ├── _header.scss │ ├── _mixins.scss │ ├── _navigator.scss │ ├── _relatedarticleslist.scss │ ├── _resets.scss │ ├── _variables.scss │ └── main.scss └── templates │ ├── about.html │ ├── archivepage.html │ ├── articlepage.html │ ├── blogpage.html │ ├── categorypage.html │ └── index.html └── trio.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parserOptions": { 4 | "sourceType": "module" 5 | }, 6 | "rules": { 7 | "indent": [ 8 | "error", 9 | 4 10 | ], 11 | "linebreak-style": [ 12 | "error", 13 | "unix" 14 | ], 15 | "quotes": [ 16 | "error", 17 | "double" 18 | ], 19 | "semi": [ 20 | "error", 21 | "always" 22 | ], 23 | "no-console": 0, 24 | "eol-last": "off", 25 | "no-mixed-operators": "off", 26 | "no-prototype-builtins": "off" 27 | } 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2021 Jeffrey Schwartz All Rights Reserved 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trio 2 | 3 | Fast, simple yet powerful JavaScript-driven static site generation. 4 | 5 | ## Documentation 6 | 7 | ### v6 8 | 9 | https://gettriossg.com/docs/v6 10 | 11 | ### Prior Versions 12 | 13 | Please note that earlier versions are no longer supported. 14 | 15 | ## Changelog 16 | 17 | Please note that beginning with v4.0.0 changelog entries now only list major highlights. 18 | 19 | ### v6.1.0 20 | 21 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.1.0+is%3Aclosed. 22 | 23 | #### Highlights! 24 | 25 | ##### Issue #137: Refactor Trio to use BrowserSync's watcher to watch for changes to the public build folder and to respond to "change" events by reloading the browser. 26 | 27 | ##### Issue #138: Auto increment the port number that BrowserSync uses since BrowserSync no longer does so when the default port number (3000) is being used. 28 | 29 | ### v6.0.4 30 | 31 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.0.4+is%3Aclosed. 32 | 33 | #### Highlights! 34 | 35 | ##### Issue #134: Module lib/generator/sassRender.js must call toString() when writing the postcssResult.map to a file. 36 | 37 | ##### Issue #135: Upgrade Node to v14 LTS and upgrade Trio's dependencies. 38 | 39 | ##### Issue #136: A circular dependency discovered in lib/utils/getFileModifiedTime.js after upgrading Node to v14 LTs and Trio's dependencies. 40 | 41 | ### v6.0.3 42 | 43 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.0.3+is%3Aclosed. 44 | 45 | #### Highlights! 46 | 47 | ##### Issue #133: File watcher not refreshing the browser when ignored file has been added, changed, and deleted. 48 | 49 | ### v6.0.2 50 | 51 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.0.2+is%3Aclosed. 52 | 53 | #### Highlights! 54 | 55 | ##### Issue #131: lib/generator/copyOtherFiles isn't checking if the source folders exist before copying files to the destination folders. 56 | 57 | ### v6.0.1 58 | 59 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.0.1+is%3Aclosed. 60 | 61 | #### Highlights! 62 | 63 | ##### Issue #127: File watcher (chokidar) should be configured to ignore user defined folders to prevent polluting metadata and unnecessary development builds. 64 | 65 | ##### Issue #128: lib/generator/getAssets should be configured to ignore user defined folders to prevent polluting metadata. 66 | 67 | ##### Issue #129: Add "ignore" property to user configuration. 68 | 69 | ### v6.0.0 70 | 71 | Please see https://github.com/4awpawz/trio/issues?q=is%3Aissue+milestone%3Av6.0.0+is%3Aclosed. 72 | 73 | #### Highlights! 74 | 75 | ##### Issue #124: Remove cache busting from the release CLI command and create its own CLI command cachebust. 76 | 77 | ##### Issue #123: Add configuration options for css source map generation. 78 | 79 | ### v5.0.0 80 | 81 | Please see https://github.com/4awpawz/trio/milestone/25?closed=1 for details. 82 | 83 | #### Highlights! 84 | 85 | ##### Issue #120: Replace Node Sass with Dart Sass. 86 | 87 | ##### Issue #121: Update all packages and address vulnerabilities. 88 | 89 | ### v4.3.1 90 | 91 | Please see https://github.com/4awpawz/trio/milestone/24?closed=1 for details. 92 | 93 | #### Highlights! 94 | 95 | ##### Issue #119: Treat all fragment file names preceded with an underscore "\_" as a work in progress. 96 | 97 | ### v4.3.0 98 | 99 | Please see https://github.com/4awpawz/trio/milestone/23?closed=1 for details. 100 | 101 | #### Highlights! 102 | 103 | ##### Issue #116: The fragment front matter _title_ property now defaults to _Document_. 104 | 105 | ##### Issue #117: Trio now does a _one-off build_ whenever a file is added to the project and building incrementally. 106 | 107 | ### v4.2.0 108 | 109 | Please see https://github.com/4awpawz/trio/issues?q=milestone%3Av4.2.0 for details. 110 | 111 | #### Highlights! 112 | 113 | ##### Issue #112: Trio now ignores and reports fragments that do not have front matter defined. 114 | 115 | ### v4.1.0 116 | 117 | Please see https://github.com/4awpawz/trio/issues?q=milestone%3Av4.1.0 for details. 118 | 119 | #### Highlights! 120 | 121 | ##### Issue #111: Trio now supports callbacks declared on tags that are replaced by include and fragment content. 122 | 123 | ### v4.0.0 124 | 125 | Please see https://github.com/4awpawz/trio/issues?q=milestone%3Av4.0.0 for details. 126 | 127 | #### Highlights! 128 | 129 | ##### Issue #102: Trio now preserves the relative paths of target child folders when resolving permalinks. 130 | 131 | ##### Issue #103: Trio now assigns meaningful default values for all the properties in site metadata and trio.manifest.json. 132 | 133 | ##### Issue #106: Trio projects now include a source/lib folder for modules that are required by tag-based callback modules and collection filter modules. 134 | 135 | ##### Issue #107: Trio's generated projects now include the empty folders that in previous versions were missing because Git doesn't track empty folders. 136 | 137 | ##### Issue: #108: Trio no longer generates spurious warning messages when validating permalinks. 138 | 139 | ##### Issue: #109: Trio no longer includes the ESLint configuration package @4awpawz/eslint-config-4awpawzconfig in scaffold projects. 140 | 141 | ### v3.1.0 142 | 143 | #### Highlights! 144 | 145 | * New! Preserved include and fragment target tags. 146 | * New! Auto detection of new version. 147 | 148 | #### More Information 149 | 150 | For more information regarding this release, please read Trio v3.1.0: Improved Developer Experience 151 | 152 | ### v3.0.0 153 | 154 | #### Highlights! 155 | 156 | * New! Permalinks. 157 | * New! You now have total control as to how you structure your blogs. 158 | * New! Incremental Build Now Watches trio.json For Changes. 159 | * New! Enhanced Collection Dataset Validation. 160 | 161 | #### Other Changes 162 | 163 | * Improved Consistent Formating For Error And Warning Messages. 164 | 165 | #### Major Bug Fixes 166 | * Fixed #98 that prevented you from creating a new project at the command line from within the new project's target folder. 167 | 168 | * Fixed #94 that would cause incremental build with watch to break when you would rename a fragment. Now, when you rename a fragment, Trio will run a one-off build to insure that its cache accurately represents the current state of your project. 169 | 170 | * Fixed #93 that would sometimes prevent articles from being generated if they didn't declare a category in their front matter. 171 | 172 | #### Upgrading From v2 to v3 173 | 174 | Whenever you upgrade to a new version of Trio, you should always run a one-off build to insure that Trio's cache is updated to its current expectations: 175 | 176 | ```shell 177 | $ trio build | b 178 | ``` 179 | 180 | #### More Information 181 | 182 | For more information regarding this release, please read Trio v3.0.0: Permalinkns And Other Goodies 183 | 184 | ### v2.0.0 185 | 186 | #### Highlights! 187 | 188 | * Collections! 189 | * Enhancements To The CLI 190 | 191 | #### Other Changes 192 | 193 | * Trio no longer appends "-map" to the output css map file name. This was cruft left over from when Buster was still appending its hash to file names, which it no longer does as it now prepends the file name with the hash. 194 | 195 | * The trio.manifest.json file is no longer compressed. 196 | 197 | * You are no longer required to include the .html file extension in the fragment's front matter template property. 198 | 199 | #### Changes To gettriossg.com 200 | 201 | There's a new home page, which more succinctly expresses what Trio is all about, and the documentation has received numerous corrections, and enhancements to make it more accurate and easier to read. 202 | 203 | #### Bug Fixes 204 | 205 | * Fixed #88 - excerpts in markdown fragments aren't being converted to HTML. 206 | 207 | * Fixed #87 - css source maps are being generated incorrectly. 208 | 209 | #### Upgrading From v1 to v2 210 | 211 | The schema for the cache has changed for v2.0.0, and is incompatible with prior versions. Therefore, if you are upgrading your projects from v1 to v2, you will have to regenerate your project's cache, which you can do by running the following build command in the terminal: 212 | 213 | ```shell 214 | $ trio build | b 215 | ``` 216 | 217 | ### v1.1.1 (IKIGAI) 218 | 219 | #### Bug Fixes 220 | 221 | * Addresses issue #84, where stats for JSON data files and modules indicate they have dependencies to callbacks even when those callbacks no longer declare them as internal dependencies. 222 | 223 | ### v1.1.0 (IKIGAI) 224 | 225 | #### Highlights! 226 | 227 | * Trio now provides more information when it is unable to resolve assets. See issue #83 for more information. 228 | 229 | #### Bug Fixes 230 | 231 | * Addresses issue #82, where integrity checking and dependency resolution failed to account for templates having indirect references to includes, which caused builds to fail during page generation because of missing include files. 232 | 233 | ### v1.0.0 (IKIGAI) 234 | 235 | This marks the first stable release of v1.0.0. I'm so excited and I hope you all are too. 236 | 237 | #### Highlights! 238 | 239 | * Project scaffolding (issue #80) is now supported as an option (i.e. `-s` | `--scaffold`) when creating a new project from the command line (e.g. `$ trio new -s path/to/new/project`). 240 | 241 | ### v1.0.0-rc.6 (IKIGAI) 242 | 243 | #### Highlights! 244 | 245 | * Addresses issue #79 which formalizes blog archive pages into the project just like blog tag and blog category pages are. 246 | 247 | #### Bug Fixes 248 | 249 | * Addresses issue #81 which would cause destination paths for blog subsidiary pages and archive pages to be generated incorrectly. 250 | 251 | ### v1.0.0-rc.5 (IKIGAI) 252 | 253 | #### Highlights! 254 | 255 | * For callbacks that throw exceptions, Trio would catch the exceptions and print just their message to the console, leaving it up to the user to determine which module actually threw the exception. Now, when callbacks throw exceptions, Trio will also print their module names along with their messages to the console. 256 | 257 | #### Bug Fixes 258 | 259 | * Breaking Change - Fragment properties `destPath` (the generated page's target file path) and `url` (the generated page's URL) are now generated in all lowercase. 260 | 261 | * Breaking Change - Trio no longer generates a unique `id` property for fragments due to the expense in terms of the development time required to maintain their integrity during incremental builds. In its place, users should now use the fragment's `url` property, which is intrinsically always unique. 262 | 263 | * Breaking Change - Callback argument `$` has been renamed to `$page` for clarity and consistency. 264 | 265 | ### v1.0.0-rc.4 (IKIGAI) 266 | 267 | #### Bug Fixes 268 | 269 | * Addresses issue #74 which would cause garbage collection to fail to identify the _original_ blog article to delete when the user changes the blog article's category. 270 | 271 | ### v1.0.0-rc.3 (IKIGAI) 272 | 273 | #### Bug Fixes 274 | 275 | * Addresses issue #73 which would cause dependency resolution to fail if tag-based callbacks are declared with their `.js` file extensions. 276 | 277 | ### v1.0.0-rc.2 (IKIGAI) 278 | 279 | #### Bug Fixes 280 | 281 | * Addresses issue #72 which would cause include assets with `.html` file extension to be wrongly interpreted as markdown files. 282 | 283 | ### v1.0.0-rc.1 (IKIGAI) 284 | 285 | This marks the first release candidate for Trio v1. The journey from v0.0.6, which served as a solid proof of concept, to now, has been a long and sometimes difficult one, but in the end I can truly say that it has been a labor of love. I hope you all enjoy it. 286 | 287 | #### Highlights! 288 | 289 | * Integrity Checking checks your project's chains of dependencies and notifies you when assets can't be resolved. 290 | 291 | * Incremental Build reduces project build times by limiting processing to only stale assets. 292 | 293 | * Tag-Based JavaScript Callbacks replace the template engines and frameworks that other static site generators require you to use to extend your composites. 294 | 295 | ### v0.0.6 296 | 297 | #### Bug Fixes 298 | 299 | * Addresses issue #59 which would cause etc folder processing to ignore dot files. 300 | 301 | ### v0.0.5 302 | 303 | #### Bug Fixes 304 | 305 | * Addresses issue #58 which would raise an exception when generating the public destination paths for category pages. 306 | 307 | ### v0.0.4 308 | 309 | #### Bug Fixes 310 | 311 | * Addresses issue #57 which adds the `source/etc` folder to generated projects and whose files are copied as is to the root of the public folder for both dev and release builds. This folder is intended to be used for files like `favicon.ico, robots.txt, sitemaps, .nojekyll, .etc` which need to reside in the public folder. 312 | 313 | 314 | ### v0.0.3 315 | 316 | #### Bug Fixes 317 | 318 | * Addresses issue #56 which adds a new configuration option, `"nojekyll"`, which when set to `true` instructs Trio to write a `.nojekyll` file to the public folder when generating a release build to completely bypass Jekyll processing on GitHub Pages. 319 | 320 | ### v0.0.2 321 | 322 | #### Bug Fixes 323 | 324 | * Addresses issue #55 which would cause the generation of inaccurate public paths for blog articles that have nested categories. 325 | 326 | * Addresses issue #54 which would cause the generation of inaccurate public paths for blog articles that have complex names. 327 | 328 | ## Copyright And License 329 | 330 | Code and documentation Copyright ©2018-2021 Jeffrey Schwartz All Rights Reserved 331 | 332 | Code licensed MIT, docs CC By 3.0. -------------------------------------------------------------------------------- /bin/trio.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | /** 4 | * Command line options are specified following the GNU specification 5 | * (see http://www.catb.org/~esr/writings/taoup/html/ch10s05.html for details). 6 | */ 7 | 8 | const log = require("../lib/utils/log"); 9 | const { readCache } = require("../lib/utils/readCache"); 10 | const fileNames = require("../lib/config/fileNames"); 11 | const { version } = require("../package.json"); 12 | 13 | process.env.TRIO_ENV_version = version; 14 | 15 | // get all of the options and normalize combined options, such as from ["-wi]" to ["-w", "-i"] 16 | const options = []; 17 | process.argv.slice(2) 18 | .filter(arg => arg[0] === "-") 19 | .reduce((accum, value) => { 20 | if (value.startsWith("--")) { 21 | accum.push(value); 22 | return accum; 23 | } else { 24 | [...value].forEach(item => { 25 | if (item !== "-") { 26 | accum.push(`-${item}`); 27 | } 28 | }); 29 | return accum; 30 | } 31 | }, options); 32 | 33 | // get all of the commands and arguments 34 | const commands = process.argv 35 | .slice(2) 36 | .filter(arg => arg[0] !== "-"); 37 | 38 | const getBaseUrl = () => readCache(fileNames.userConfigFileName).baseUrl || ""; 39 | 40 | // prints generalized help to stdout 41 | const generalHelp = () => { 42 | log(""); 43 | log("Usage: trio [option] | trio [command] [args]"); 44 | log(""); 45 | log("where [option] is one of:"); 46 | log(" -v | --version (version)"); 47 | log(" -h | --help (this help)"); 48 | log(""); 49 | log("where [command] is one of:"); 50 | log(" n, new, b, build, r, release, B, Bust, s, serve"); 51 | log(""); 52 | log("For command specific help, enter trio -h | --help [command]"); 53 | log(""); 54 | }; 55 | 56 | // prints command specific help to stdout 57 | const commandSpecificHelp = (command) => { 58 | if (command === "b" || command === "build") { 59 | log("NAME"); 60 | log(" trio-build - Builds your site."); 61 | log(""); 62 | log("SYNOPSIS"); 63 | log(" trio build [options]"); 64 | log(""); 65 | log(" alias: trio b"); 66 | log(""); 67 | log("DESCRIPTION"); 68 | log(" This command builds your site targeting the public folder."); 69 | log(""); 70 | log(" trio build"); 71 | log(" trio build [-i | --incremental-build]"); 72 | log(" trio build [-w | --watch]"); 73 | log(" trio build [-i | --incremental-build] [-w | --watch]"); 74 | log(" trio build [-w | --watch] [-s | --serve]"); 75 | log(" trio build [-i | --incremental-build] [-w | --watch] [-s | -serve]"); 76 | log(""); 77 | log(" In the first form, it builds your entire site."); 78 | log(""); 79 | log(" In the second form, it incrementally builds your site."); 80 | log(""); 81 | log(" In the third form, it builds your entire site any time a change is made to"); 82 | log(" a file in the source folder."); 83 | log(""); 84 | log(" In the fourth form, it incrementally builds your site any time a change"); 85 | log(" is made to a file in the source folder."); 86 | log(""); 87 | log(" In the fifth form, it builds your entire site any time a change is made to"); 88 | log(" a file in the source folder and serves it in the default browser."); 89 | log(""); 90 | log(" In the sixth form, it incrementally builds your site any time a change"); 91 | log(" to a file in the source folder and serves it in the default browser."); 92 | log(""); 93 | log("OPTIONS"); 94 | log(" -i | --incremental-build"); 95 | log(" Builds the site incrementally."); 96 | log(""); 97 | log(" -w | --watch"); 98 | log(" Watches for changes to files in the source folder."); 99 | log(""); 100 | log(" -s | --serve"); 101 | log(" Serves the site in the default browser."); 102 | log(""); 103 | log(" -I"); 104 | log(" Shortcut for -iws."); 105 | log(""); 106 | log(" -m"); 107 | log(" Prints detailed metrics."); 108 | log(""); 109 | } else if (command === "n" || command === "new") { 110 | log("NAME"); 111 | log(" trio-new - Create a new project."); 112 | log(""); 113 | log("SYNOPSIS"); 114 | log(" trio new [options] [path]"); 115 | log(""); 116 | log(" alias: trio n"); 117 | log(""); 118 | log("DESCRIPTION"); 119 | log(" This command creates a new project in the path folder. This command will"); 120 | log(" abort with an error message if the path folder already exists or if path is invalid"); 121 | log(" or the path is omitted."); 122 | log(""); 123 | log(" trio new [path]"); 124 | log(" trio new [-s | --scaffold] [path]"); 125 | log(""); 126 | log(" In the first form, it creates a new bare project in the path folder."); 127 | log(""); 128 | log(" In the second form, it creates a new project with scaffolding in the path folder."); 129 | log(""); 130 | log("OPTIONS"); 131 | log(" -s | --scaffold"); 132 | log(" Creates a new project with scaffolding."); 133 | log(""); 134 | } else if (command === "r" || command === "release") { 135 | log("NAME"); 136 | log(" trio-release - Builds your site for release."); 137 | log(""); 138 | log("SYNOPSIS"); 139 | log(" trio release [options]"); 140 | log(""); 141 | log(" alias: r"); 142 | log(""); 143 | log("DESCRIPTION"); 144 | log(" This command builds your site for release targeting the release folder."); 145 | log(""); 146 | log(" trio release"); 147 | log(""); 148 | log(" In the first and only form, it builds your site for release."); 149 | log(""); 150 | log("OPTIONS"); 151 | log(" -m"); 152 | log(" Prints detailed metrics."); 153 | log(""); 154 | } else if (command === "c" || command === "cachebust") { 155 | log("NAME"); 156 | log(" trio-cachebust - Cache busts the release build residing in the release folder."); 157 | log(""); 158 | log("SYNOPSIS"); 159 | log(" trio cachebust [options]"); 160 | log(""); 161 | log(" alias: c"); 162 | log(""); 163 | log("DESCRIPTION"); 164 | log(" This command cache busts the release build residing in the release folder."); 165 | log(""); 166 | log(" trio cachebust"); 167 | log(" trio cachebust [-v | --verbose]"); 168 | log(" trio cachebust [-m | --manifest]"); 169 | log(" trio cachebust [-v | --verbose m] [-m | manifest]"); 170 | log(""); 171 | log(" In the first form, it cache busts the release build residing in the release folder."); 172 | log(" In the second form, it cache busts the release build residing in the release folder"); 173 | log(" and provides verbose cache busting details."); 174 | log(" In the third form, it cache busts the release build residing in the release folder"); 175 | log(" and provides a manifest."); 176 | log(" In the fourth form, it cache busts the release build residing in the release folder"); 177 | log(" and provides both verbose cache busting details and a manifest."); 178 | log(""); 179 | log("OPTIONS"); 180 | log(" -v | --verbose"); 181 | log(" Provides verbose cache busting details."); 182 | log(" -m | --manifest"); 183 | log(" Provides a manifest."); 184 | log(" -V"); 185 | log(" Shortcut for -vm."); 186 | log(""); 187 | } else if (command === "s" || command === "serve") { 188 | log("NAME"); 189 | log(" trio-serve - Serves your site in the default browser."); 190 | log(""); 191 | log("SYNOPSIS"); 192 | log(" trio serve [options]"); 193 | log(""); 194 | log(" alias: s"); 195 | log(""); 196 | log("DESCRIPTION"); 197 | log(" This command serves the site in the default browser."); 198 | log(""); 199 | log(" trio serve"); 200 | log(" trio serve -r"); 201 | log(""); 202 | log(" In the first form, it serves your site from the public folder"); 203 | log(""); 204 | log(" In the second form, it serves your site from the release folder"); 205 | log(""); 206 | log("OPTIONS"); 207 | log(" -r | --release"); 208 | log(" Serves your site from the release folder."); 209 | log(""); 210 | } else { 211 | generalHelp(); 212 | } 213 | }; 214 | 215 | /** 216 | * command validation and execution 217 | */ 218 | 219 | const newCommandParams = { 220 | opts: ["-s", "--scaffold"], 221 | validate: function ({ commands, options }) { 222 | if (commands.length > 2 || options.length > 1) { 223 | return false; 224 | } 225 | if (options.length > 0 && !options.every(opt => this.opts.includes(opt))) { 226 | return false; 227 | } 228 | return true; 229 | }, 230 | valid: ({ commands, options }) => { 231 | const createNewProject = require("../lib/tasks/create-new-project"); 232 | createNewProject(commands[1], options); 233 | }, 234 | invalid: () => generalHelp() 235 | }; 236 | 237 | const buildCommandParams = { 238 | opts: ["-w", "--watch", "-i", "--incremental-build", "-s", "--serve", "-I", "-m"], 239 | validate: function ({ commands, options }) { 240 | if (commands.length > 1 || options.length > 4) { 241 | return false; 242 | } 243 | if (options.length > 0 && !options.every(opt => this.opts.includes(opt))) { 244 | return false; 245 | } 246 | return true; 247 | }, 248 | valid: async ({ options }) => { 249 | const build = require("../index"); 250 | const watch = require("../lib/tasks/file-watcher"); 251 | process.env.TRIO_ENV_buildType = "development"; 252 | process.env.TRIO_ENV_printMetrics = 253 | options.some(opt => 254 | opt === "-m") 255 | ? "print-metrics" 256 | : "no-print-metrics"; 257 | process.env.TRIO_ENV_serveInBrowser = 258 | options.some(opt => 259 | opt === "-s" || opt === "--serve" || opt === "-I") 260 | ? "serve-in-browser" 261 | : "no-serve-in-browser"; 262 | process.env.TRIO_ENV_buildIncrementally = 263 | options.some(opt => 264 | opt === "-i" || opt === "--incremental-build" || opt === "-I") 265 | ? "incremental-build" 266 | : "no-incremental-build"; 267 | process.env.TRIO_ENV_watching = 268 | options.some(opt => 269 | opt === "-w" || opt === "--watch" || opt === "-I") 270 | ? "watch" 271 | : "no-watch"; 272 | if (process.env.TRIO_ENV_serveInBrowser === "serve-in-browser") { 273 | require("../lib/utils/browserSync")(); 274 | } 275 | await build(); 276 | if (process.env.TRIO_ENV_watching === "watch") { 277 | watch(); 278 | } 279 | }, 280 | invalid: () => generalHelp() 281 | }; 282 | 283 | const serveCommandParams = { 284 | opts: ["-r", "--release"], 285 | validate: function ({ commands, options }) { 286 | if (commands.length > 1 || options.length > 1) { 287 | return false; 288 | } 289 | if (options.length > 0 && !options.every(opt => this.opts.includes(opt))) { 290 | return false; 291 | } 292 | return true; 293 | }, 294 | valid: async ({ options }) => { 295 | process.env.TRIO_ENV_serveInBrowser = "serve-in-browser"; 296 | if (options.length === 1) { 297 | process.env.TRIO_ENV_buildType = "release"; 298 | // note: config is required here just for side effects, specifically so that it 299 | // correctly sets the public folder to either "./public" or "./release", which 300 | // BrowserSync will use as the site's base directory 301 | require("../lib/config"); 302 | } 303 | // Since v3 process.env.TRIO_ENV_baseUrl is no longer set by configuration and is 304 | // now set directly by userConfig, which is run by the generator which is not run 305 | // for the serve command as there is nothing to generate. Therefore, it must 306 | // be set here for browsersync to work correctly. 307 | process.env.TRIO_ENV_baseUrl = getBaseUrl(); 308 | require("../lib/utils/browserSync")(); 309 | }, 310 | invalid: () => generalHelp() 311 | }; 312 | 313 | const releaseCommandParams = { 314 | opts: ["-m"], 315 | validate: function ({ commands, options }) { 316 | if (commands.length > 1 || options.length > 1) { 317 | return false; 318 | } 319 | if (options.length > 0 && !options.every(opt => this.opts.includes(opt))) { 320 | return false; 321 | } 322 | return true; 323 | }, 324 | valid: async () => { 325 | process.env.TRIO_ENV_buildType = "release"; 326 | process.env.TRIO_ENV_serveInBrowser = "no-serve-in-browser"; 327 | process.env.TRIO_ENV_buildIncrementally = "no-incremental-build"; 328 | process.env.TRIO_ENV_watching = "no-watch"; 329 | process.env.TRIO_ENV_printMetrics = 330 | options.some(opt => 331 | opt === "-m") 332 | ? "print-metrics" 333 | : "no-print-metrics"; 334 | const build = require("../index"); 335 | await build(); 336 | }, 337 | invalid: () => generalHelp() 338 | }; 339 | 340 | const cacheBustCommandParams = { 341 | opts: ["-v", "--verbose", "-m", "--manifest", "-V"], 342 | validate: function ({ commands, options }) { 343 | if (commands.length > 1 || options.length > 2) { 344 | return false; 345 | } 346 | if (options.length > 0 && !options.every(opt => this.opts.includes(opt))) { 347 | return false; 348 | } 349 | return true; 350 | }, 351 | valid: async () => { 352 | const cacheBust = require("../lib/tasks/cache-bust/cacheBust"); 353 | await cacheBust(options).catch(e => { 354 | console.log("Something went wrong. Try running 'trio -r' first and then try again."); 355 | console.log(e); 356 | }); 357 | }, 358 | invalid: () => generalHelp() 359 | }; 360 | 361 | const validCommandOptions = new Map(); 362 | validCommandOptions.set("new", newCommandParams); 363 | validCommandOptions.set("n", newCommandParams); 364 | validCommandOptions.set("build", buildCommandParams); 365 | validCommandOptions.set("b", buildCommandParams); 366 | validCommandOptions.set("serve", serveCommandParams); 367 | validCommandOptions.set("s", serveCommandParams); 368 | validCommandOptions.set("release", releaseCommandParams); 369 | validCommandOptions.set("r", releaseCommandParams); 370 | validCommandOptions.set("cachebust", cacheBustCommandParams); 371 | validCommandOptions.set("c", cacheBustCommandParams); 372 | 373 | // command runner 374 | (async () => { 375 | if (commands.length === 0 && options[0] === "-v" || options[0] === "--version") { 376 | log(version); 377 | log(""); 378 | } else if (options[0] === "-h" || options[0] === "--help") { 379 | if (commands[0]) { 380 | commandSpecificHelp(commands[0]); 381 | } else { 382 | generalHelp(); 383 | } 384 | } else { 385 | const commandParams = validCommandOptions.get(commands[0]); 386 | if (commandParams) { 387 | if (commandParams.validate({ commands, options })) { 388 | await commandParams.valid({ commands, options }); 389 | } else { 390 | commandParams.invalid(); 391 | } 392 | } else { 393 | generalHelp(); 394 | } 395 | } 396 | } 397 | )(); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const generate = require("./lib/generator"); 3 | const { log } = require("./lib/utils"); 4 | 5 | module.exports = async (path) => { 6 | const seperator = "*********************************************************"; 7 | log(seperator); 8 | if (process.env.TRIO_ENV_buildType === "release") { 9 | log("building site for release"); 10 | } else { 11 | log("building site for development"); 12 | } 13 | await generate(path).catch((e) => { log(e); }); 14 | log(seperator); 15 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/fragmentFrontMatterCheck/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Report all the fragments that don't have front matter and delete them from the assets array. 4 | */ 5 | 6 | const config = require("../../config"); 7 | const { deleteCachedMatter, getCachedMatter, log } = require("../../utils"); 8 | 9 | module.exports = (assets) => { 10 | const targets = []; 11 | assets.forEach((path, index) => { 12 | var isFragment = path.startsWith(config.fragments); 13 | var fragmentHasFrontMatter = isFragment && Object.keys(getCachedMatter(path).data).length > 0; 14 | isFragment && !fragmentHasFrontMatter && targets.push({ path, index }); 15 | }); 16 | targets.length > 0 && log(`Warning: The following ${targets.length} fragments will be ignored because they are missing front matter:`); 17 | targets.forEach(target => log(` ${target.path}`)); 18 | // items in the assets array must be deleted in reverse or else it will throw off the index by 1 for each deletion 19 | targets.reverse().forEach(({ index }) => assets.splice(index, 1)); 20 | // clean up caches 21 | targets.forEach(({ path }) => deleteCachedMatter(path)); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/buildanalyzer/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | fragmentFrontMatterCheck: require("./fragmentFrontMatterCheck"), 4 | integrityCheck: require("./integrityCheck"), 5 | resolveDependencies: require("./resolveDependencies") 6 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/integrityCheck/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Validates that all referenced assets in user's project exist. 4 | */ 5 | 6 | const { existsSync } = require("fs-extra"); 7 | const { join, parse, sep } = require("path"); 8 | const config = require("../../config"); 9 | const cheerio = require("cheerio"); 10 | const { 11 | getAllIncludes, 12 | getCachedMatter, 13 | getTagCallbackDependencies, 14 | getTemplateFileName, 15 | resolveIndirectPathToInclude, 16 | mdToHtml, 17 | readCache, 18 | toArray 19 | } = require("../../utils"); 20 | 21 | module.exports = (assets) => { 22 | const missingAssets = []; 23 | const noteMissingAsset = (assetPath, referencedIn) => 24 | !existsSync(assetPath) && missingAssets.push(`${assetPath} - referenced in ${referencedIn}`); 25 | const noteMissingIndirectInclude = (name, templatePath) => 26 | missingAssets.push(`indirect include ${name} - referenced in ${templatePath}`); 27 | 28 | // check each fragment for missing assets 29 | assets.filter(path => path.startsWith(config.fragments)) 30 | .forEach(path => { 31 | const matter = getCachedMatter(path); 32 | const $content = cheerio.load(mdToHtml(path, matter.content)); 33 | // add ".html" to matter.data.template if user omitted it 34 | matter.data.template = getTemplateFileName(matter.data.template); 35 | // check for missing template dependency 36 | noteMissingAsset(`${config.templates}${sep}${matter.data.template}`, path); 37 | // check for missing callback dependencies 38 | const tagCallbacks = getTagCallbackDependencies($content); 39 | tagCallbacks && tagCallbacks.length > 0 && tagCallbacks.forEach(tagCallback => 40 | noteMissingAsset(`${config.callbacks}${sep}${parse(tagCallback).name}.js`, path)); 41 | // check for missing include dependencies 42 | const includes = getAllIncludes($content); 43 | includes && includes.length > 0 && includes.forEach(include => { 44 | noteMissingAsset(`${config.includes}${sep}${include}`, path); 45 | }); 46 | // if generator check for missing filter callback 47 | matter.data.collection && matter.data.collection.filterFn && 48 | noteMissingAsset(`${config.filters}${sep}${matter.data.collection.filterFn}.js`, path); 49 | }); 50 | 51 | // check each template for missing assets 52 | assets.filter(path => path.startsWith(config.templates)) 53 | .forEach(path => { 54 | const $content = cheerio.load(readCache(path)); 55 | // check for missing callback dependencies 56 | const tagCallbacks = getTagCallbackDependencies($content); 57 | tagCallbacks && tagCallbacks.length > 0 && tagCallbacks.forEach(tagCallback => 58 | noteMissingAsset(`${config.callbacks}${sep}${parse(tagCallback).name}.js`, path)); 59 | // check for missing include dependencies 60 | const includes = getAllIncludes($content); 61 | includes && includes.length > 0 && includes.forEach(include => { 62 | if (parse(include).ext === "") { 63 | // include is referenced indirectly so search fragments 64 | // for matching variable and use its value instead 65 | // *note: it is possible that the template is not referenced by any fragment so ignore it then 66 | const includePath = resolveIndirectPathToInclude(path, include, 67 | assets.filter(path => path.startsWith(config.fragments))); 68 | !includePath && noteMissingIndirectInclude(include, path); 69 | } else { 70 | noteMissingAsset(`${config.includes}${sep}${include}`, path); 71 | } 72 | }); 73 | }); 74 | 75 | // check each include for missing assets 76 | assets.filter(path => path.startsWith(config.includes)) 77 | .forEach(path => { 78 | const matter = getCachedMatter(path); 79 | const $content = cheerio.load(mdToHtml(path, matter.content)); 80 | // check for missing callback dependencies 81 | const tagCallbacks = getTagCallbackDependencies($content); 82 | tagCallbacks && tagCallbacks.length > 0 && tagCallbacks.forEach(tagCallback => 83 | noteMissingAsset(`${config.callbacks}${sep}${parse(tagCallback).name}.js`, path)); 84 | }); 85 | 86 | // check each callback for missing assets 87 | assets.filter(path => parse(path).dir === config.callbacks) 88 | .forEach(path => { 89 | const matter = getCachedMatter(path, { delimiters: config.jsFrontMatterDelimiters }); 90 | // check for missing module dependencies 91 | const moduleDependencies = toArray(matter.data.moduleDependencies); 92 | moduleDependencies && moduleDependencies.length > 0 && moduleDependencies 93 | .forEach(moduleDependency => { 94 | noteMissingAsset(join(config.callbacks, moduleDependency) + ".js", path); 95 | }); 96 | // check for missing data dependencies 97 | const dataDependencies = toArray(matter.data.dataDependencies); 98 | dataDependencies && dataDependencies.length > 0 && dataDependencies 99 | .forEach(dataDependency => { 100 | noteMissingAsset(join(config.sourceData, dataDependency) + ".json", path); 101 | }); 102 | }); 103 | 104 | return missingAssets; 105 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/groupStatsByCategory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Groups stale stats by type into arrays 4 | * and returns those in an object. 5 | */ 6 | 7 | module.exports = stats => { 8 | const categorized = { 9 | generators: [], 10 | fragments: [], 11 | includes: [], 12 | templates: [], 13 | callbacks: [], 14 | modules: [], 15 | filters: [], 16 | data: [], 17 | "*": [] 18 | }; 19 | stats.forEach(element => { 20 | const category = 21 | element.type === "generator" && "generators" || 22 | element.type === "fragment" && "fragments" || 23 | element.type === "template" && "templates" || 24 | element.type === "include" && "includes" || 25 | element.type === "callback" && "callbacks" || 26 | element.type === "module" && "modules" || 27 | element.type === "filter" && "filters" || 28 | element.type === "data" && "data" || 29 | element.type === "*" && "*"; 30 | categorized[category].push(element); 31 | }); 32 | return categorized; 33 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Resolves all stale assets to fragments and includes using their dependency paths. 4 | */ 5 | 6 | const groupStatsByCategory = require("./groupStatsByCategory"); 7 | const { 8 | getTemplateDeps, 9 | getIncludeDeps, 10 | getCallbackDeps, 11 | getRequiredModuleDeps, 12 | getDataDeps, 13 | getAllIncludeDepsFromResolvedFragment, 14 | getFilterDeps 15 | } = require("./resolvers"); 16 | 17 | module.exports = (stats) => { 18 | const stale = process.env.TRIO_ENV_buildIncrementally === "no-incremental-build" 19 | ? stats 20 | : stats.filter(stat => stat.isStale); 21 | 22 | // categorize stats by their type 23 | const getAllPathsByType = (stats => { 24 | return type => stats 25 | .filter(stat => stat.type === type) 26 | .map(stat => stat.path); 27 | })(stats); 28 | 29 | // paths to all generators 30 | const pathsToAllGenerators = getAllPathsByType("generator"); 31 | // paths to all fragments 32 | const pathsToAllFrags = getAllPathsByType("fragment"); 33 | // paths to all templates 34 | const pathsToAllTemplates = getAllPathsByType("template"); 35 | // paths to all includes 36 | const pathsToAllIncludes = getAllPathsByType("include"); 37 | // paths to all callbacks 38 | const pathsToAllCallbacks = getAllPathsByType("callback"); 39 | 40 | stale.filter(stat => stat.type === "template") 41 | .forEach(templateStat => 42 | getTemplateDeps(templateStat, [...pathsToAllGenerators, ...pathsToAllFrags])); 43 | stale.filter(stat => stat.type === "include") 44 | .forEach(includeStat => 45 | getIncludeDeps(includeStat, pathsToAllTemplates, [...pathsToAllGenerators, ...pathsToAllFrags])); 46 | stale.filter(stat => stat.type === "callback") 47 | .forEach(callbackStat => 48 | getCallbackDeps(callbackStat, pathsToAllIncludes, pathsToAllTemplates, 49 | [...pathsToAllGenerators, ...pathsToAllFrags])); 50 | stale.filter(stat => stat.type === "module") 51 | .forEach(moduleStat => 52 | getRequiredModuleDeps(moduleStat, pathsToAllCallbacks, pathsToAllIncludes, pathsToAllTemplates, [...pathsToAllGenerators, ...pathsToAllFrags])); 53 | stale.filter(stat => stat.type === "filter") 54 | .forEach(filterStat => 55 | getFilterDeps(filterStat, pathsToAllGenerators)); 56 | stale.filter(stat => stat.type === "data") 57 | .forEach(dataStat => 58 | getDataDeps(dataStat, pathsToAllCallbacks, pathsToAllIncludes, pathsToAllTemplates, 59 | [...pathsToAllGenerators, ...pathsToAllFrags])); 60 | 61 | const staleCat = groupStatsByCategory(stale); 62 | 63 | // gather all the fragment dependencies into a set to dedupe 64 | const resolvedFragmentsSet = new Set(); 65 | const addToResolvedFragmentsSet = 66 | (set => items => items.forEach(item => set.add(item)))(resolvedFragmentsSet); 67 | 68 | addToResolvedFragmentsSet(staleCat.generators 69 | .map(generator => generator.path)); 70 | addToResolvedFragmentsSet(staleCat.fragments 71 | .map(fragment => fragment.path)); 72 | addToResolvedFragmentsSet(staleCat.templates 73 | .reduce((accum, template) => accum.concat(template.fragDeps), [])); 74 | addToResolvedFragmentsSet(staleCat.includes 75 | .reduce((accum, include) => accum.concat(include.fragDeps), [])); 76 | addToResolvedFragmentsSet(staleCat.callbacks 77 | .reduce((accum, callback) => accum.concat(callback.fragDeps), [])); 78 | addToResolvedFragmentsSet(staleCat.modules 79 | .reduce((accum, module) => accum.concat(module.fragDeps), [])); 80 | addToResolvedFragmentsSet(staleCat.filters 81 | .reduce((accum, filter) => accum.concat(filter.fragDeps), [])); 82 | addToResolvedFragmentsSet(staleCat.data 83 | .reduce((accum, data) => accum.concat(data.fragDeps), [])); 84 | 85 | // gather all the include dependencies into a set to dedupe 86 | const resolvedIncludesSet = new Set(); 87 | const addToResolvedIncludesSet = 88 | (set => items => items.forEach(item => set.add(item)))(resolvedIncludesSet); 89 | 90 | addToResolvedIncludesSet(staleCat.includes 91 | .map(include => include.path)); 92 | const includeDeps = Array.from(resolvedFragmentsSet) 93 | .reduce((accum, fragment) => accum 94 | .concat(getAllIncludeDepsFromResolvedFragment(fragment, pathsToAllTemplates, 95 | pathsToAllIncludes)), []); 96 | addToResolvedIncludesSet(includeDeps); 97 | 98 | // return object containing properties for resolved dependencies 99 | return { 100 | fragmentStats: stats.filter(stat => resolvedFragmentsSet.has(stat.path)), 101 | includeStats: stats.filter(stat => resolvedIncludesSet.has(stat.path)) 102 | }; 103 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getAllIncludeDepsFromResolvedFragment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Find all the includes referenced by a resolved 4 | * fragment and its associated template 5 | */ 6 | 7 | const { parse } = require("path"); 8 | const cheerio = require("cheerio"); 9 | const { 10 | getAllIncludes, 11 | getCachedMatter, 12 | mdToHtml, 13 | readCache 14 | } = require("../../../utils"); 15 | 16 | module.exports = (resolvedFragmentPath, pathsToAllTemplates, pathsToAllIncludes) => { 17 | const fragmentMatter = getCachedMatter(resolvedFragmentPath); 18 | let includeBaseNames = 19 | getAllIncludes(cheerio.load(mdToHtml(resolvedFragmentPath, readCache(resolvedFragmentPath)))); 20 | const templateBaseName = fragmentMatter.data.template; 21 | const templatePath = pathsToAllTemplates.find(path => parse(path).base === templateBaseName); 22 | includeBaseNames = includeBaseNames.concat(getAllIncludes(cheerio.load(readCache(templatePath))) 23 | .map(name => parse(name).ext !== "" ? name : fragmentMatter.data[name])); 24 | return pathsToAllIncludes.filter(path => includeBaseNames.includes(parse(path).base)); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getCallbackDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the fragments that depend on a stale callback file. 4 | */ 5 | 6 | const { parse } = require("path"); 7 | const cheerio = require("cheerio"); 8 | const { 9 | getAllIncludes, 10 | getCachedMatter, 11 | getTagCallbackDependencies, 12 | mdToHtml, 13 | readCache 14 | } = require("../../../utils"); 15 | 16 | module.exports = (callbackStat, pathsToAllIncludes, pathsToAllTemplates, pathsToAllFrags) => { 17 | callbackStat.includeDeps = pathsToAllIncludes 18 | .filter(path => { 19 | // check if include references callback 20 | const m = getCachedMatter(path); 21 | return getTagCallbackDependencies(cheerio.load(mdToHtml(path, m.content))) 22 | .map(path => parse(path).name) 23 | .includes(parse(callbackStat.path).name); 24 | }); 25 | callbackStat.templateDeps = pathsToAllTemplates 26 | .filter(path => { 27 | const $content = cheerio.load(readCache(path)); 28 | const condition1 = () => 29 | // check if template references an include that references callback 30 | getAllIncludes($content) 31 | .some(templateInclude => callbackStat.includeDeps 32 | .map(path => parse(path).base) 33 | .includes(templateInclude)); 34 | const condition2 = () => 35 | // check if template references callback 36 | getTagCallbackDependencies($content) 37 | .map(path => parse(path).name) 38 | .includes(parse(callbackStat.path).name); 39 | // return condition1() || condition2() 40 | return condition1() || condition2(); 41 | }); 42 | callbackStat.fragDeps = pathsToAllFrags 43 | .filter(path => { 44 | const m = getCachedMatter(path); 45 | const condition1 = () => 46 | // check if fragment is associated with a template 47 | // that references an include that references callback 48 | callbackStat.templateDeps 49 | .map(template => parse(template).base) 50 | .includes(m.data.template); 51 | const condition2 = () => 52 | // check if fragment references an include that references callback 53 | getAllIncludes(cheerio.load(mdToHtml(path, m.content))) 54 | .some(fragmentInclude => callbackStat.includeDeps 55 | .map(path => parse(path).base) 56 | .includes(fragmentInclude)); 57 | const condition3 = () => 58 | // check if fragment references callback 59 | getTagCallbackDependencies(cheerio.load(mdToHtml(path, m.content))) 60 | .map(path => parse(path).name) 61 | .includes(parse(callbackStat.path).name); 62 | // return condition1() || condition2() || condition3() || condition4(); 63 | return condition1() || condition2() || condition3(); 64 | }); 65 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getDataDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the fragments that depend on a JSON data file. 4 | */ 5 | 6 | const { join, parse } = require("path"); 7 | const cheerio = require("cheerio"); 8 | const config = require("../../../config"); 9 | const { 10 | getAllIncludes, 11 | getCachedMatter, 12 | getTagCallbacks, 13 | mdToHtml, 14 | readCache, 15 | toArray 16 | } = require("../../../utils"); 17 | 18 | module.exports = (dataStat, pathsToAllCallbacks, pathsToAllIncludes, pathsToAllTemplates, pathsToAllFrags) => { 19 | dataStat.callbackDeps = pathsToAllCallbacks 20 | .filter(path => { 21 | // check if callback references the JSON data file 22 | const matter = getCachedMatter(path, { delimiters: config.jsFrontMatterDelimiters }); 23 | const dataDependencies = toArray(matter.data.dataDependencies); 24 | return dataDependencies && dataDependencies.length > 0 && dataDependencies 25 | .some(dataDependency => 26 | join(config.sourceData, dataDependency) + ".json" === dataStat.path); 27 | }); 28 | dataStat.includeDeps = pathsToAllIncludes 29 | .filter(path => { 30 | // check if include references any of the callbacks 31 | const m = getCachedMatter(path); 32 | return getTagCallbacks(cheerio.load(mdToHtml(path, m.content))) 33 | .map(tagCallback => tagCallback.callback) 34 | .some(callback => dataStat.callbackDeps 35 | .map(path => parse(path).name) 36 | .includes(callback)); 37 | }); 38 | dataStat.templateDeps = pathsToAllTemplates 39 | .filter(path => { 40 | // check if the template references the callback 41 | const condition1 = () => getTagCallbacks(cheerio.load(readCache(path))) 42 | .map(tagCallback => tagCallback.callback) 43 | .some(callback => dataStat.callbackDeps 44 | .map(path => parse(path).name) 45 | .includes(callback)); 46 | // check if template references an include that references callback 47 | const condition2 = () => getAllIncludes(cheerio.load(readCache(path))) 48 | .some(templateInclude => dataStat.includeDeps 49 | .map(path => parse(path).base) 50 | .includes(templateInclude)); 51 | return condition1() || condition2(); 52 | }); 53 | dataStat.fragDeps = pathsToAllFrags 54 | .filter(path => { 55 | const m = getCachedMatter(path); 56 | const condition1 = () => 57 | // check if fragment references any of the callbacks 58 | getTagCallbacks(cheerio.load(mdToHtml(path, m.content))) 59 | .map(tagCallback => tagCallback.callback) 60 | .some(callback => dataStat.callbackDeps 61 | .map(path => parse(path).name) 62 | .includes(callback)); 63 | const condition2 = () => 64 | // check if fragment is associated with a template 65 | // that references an include that references callback 66 | dataStat.templateDeps 67 | .map(template => parse(template).base) 68 | .includes(m.data.template); 69 | const condition3 = () => 70 | // check if fragment references an include that references callback 71 | getAllIncludes(cheerio.load(mdToHtml(path, m.content))) 72 | .some(fragmentInclude => dataStat.includeDeps 73 | .map(path => parse(path).base) 74 | .includes(fragmentInclude)); 75 | return condition1() || condition2() || condition3(); 76 | }); 77 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getFilterDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the generators that depend on a filter file. 4 | */ 5 | 6 | const { parse } = require("path"); 7 | const { 8 | getCachedMatter 9 | } = require("../../../utils"); 10 | 11 | module.exports = (filterStat, pathsToAllGenerators) => { 12 | filterStat.fragDeps = pathsToAllGenerators 13 | .filter(path => { 14 | const m = getCachedMatter(path); 15 | // check if the generator is of type "data" and it's filter references callback 16 | return m.data.collection && m.data.collection.type === "data" && 17 | m.data.collection.filter === parse(filterStat.path).name; 18 | }); 19 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getIncludeDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the fragments that depend on a stale include file. 4 | */ 5 | 6 | const { parse } = require("path"); 7 | const cheerio = require("cheerio"); 8 | const { 9 | getAllIncludes, 10 | getCachedMatter, 11 | mdToHtml, 12 | readCache, 13 | resolveIndirectPathToInclude 14 | } = require("../../../utils"); 15 | 16 | module.exports = (includeStat, pathsToAllTemplates, pathsToAllFrags) => { 17 | includeStat.templateDeps = pathsToAllTemplates.filter(path => { 18 | const base = parse(includeStat.path).base; 19 | const includes = getAllIncludes(cheerio.load(readCache(path))); 20 | // check if template directly references include in its content 21 | const condition1 = () => 22 | includes.includes(base); 23 | // check if template indirectly references include in its content 24 | const condition2 = () => { 25 | const indirectNames = includes.filter(include => parse(include).ext === ""); 26 | return indirectNames.some(indirectName => { 27 | const resolved = resolveIndirectPathToInclude(path, indirectName, pathsToAllFrags); 28 | return resolved === base; 29 | }); 30 | }; 31 | return condition1() || condition2(); 32 | }); 33 | includeStat.fragDeps = pathsToAllFrags 34 | .filter(path => { 35 | const m = getCachedMatter(path); 36 | // checks if fragments is associated with template 37 | // via its front matter that references include 38 | const condition1 = () => 39 | includeStat.templateDeps 40 | .some(template => parse(template).base === m.data.template); 41 | // checks if fragment references include in its content 42 | const condition2 = () => 43 | getAllIncludes(cheerio.load(mdToHtml(path, m.content))) 44 | .includes(parse(includeStat.path).base); 45 | return condition1() || condition2(); 46 | }); 47 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getRequiredModuleDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the fragments that depend on a required module file. 4 | */ 5 | 6 | const { join, parse } = require("path"); 7 | const cheerio = require("cheerio"); 8 | const config = require("../../../config"); 9 | const { 10 | getAllIncludes, 11 | getCachedMatter, 12 | getTagCallbacks, 13 | mdToHtml, 14 | readCache, 15 | toArray 16 | } = require("../../../utils"); 17 | 18 | module.exports = (moduleStat, pathsToAllCallbacks, pathsToAllIncludes, pathsToAllTemplates, pathsToAllFrags) => { 19 | moduleStat.callbackDeps = pathsToAllCallbacks 20 | .filter(path => { 21 | const matter = getCachedMatter(path, { delimiters: config.jsFrontMatterDelimiters }); 22 | const moduleDependencies = toArray(matter.data.moduleDependencies); 23 | return moduleDependencies && moduleDependencies.length > 0 && moduleDependencies 24 | .some(moduleDependency => 25 | join(config.callbacks, moduleDependency) + ".js" === moduleStat.path); 26 | }); 27 | moduleStat.includeDeps = pathsToAllIncludes 28 | .filter(path => { 29 | // check if include references any of the callbacks 30 | const m = getCachedMatter(path); 31 | return getTagCallbacks(cheerio.load(mdToHtml(path, m.content))) 32 | .map(tagCallback => tagCallback.callback) 33 | .some(callback => moduleStat.callbackDeps 34 | .map(path => parse(path).name) 35 | .includes(callback)); 36 | }); 37 | moduleStat.templateDeps = pathsToAllTemplates 38 | .filter(path => { 39 | // check if the template references the callback 40 | const condition1 = () => getTagCallbacks(cheerio.load(readCache(path))) 41 | .map(tagCallback => tagCallback.callback) 42 | .some(callback => moduleStat.callbackDeps 43 | .map(path => parse(path).name) 44 | .includes(callback)); 45 | // check if template references an include that references callback 46 | const condition2 = () => getAllIncludes(cheerio.load(readCache(path))) 47 | .some(templateInclude => moduleStat.includeDeps 48 | .map(path => parse(path).base) 49 | .includes(templateInclude)); 50 | return condition1() || condition2(); 51 | }); 52 | moduleStat.fragDeps = pathsToAllFrags 53 | .filter(path => { 54 | const m = getCachedMatter(path); 55 | const condition1 = () => 56 | // check if fragment references any of the callbacks 57 | getTagCallbacks(cheerio.load(mdToHtml(path, m.content))) 58 | .map(tagCallback => tagCallback.callback) 59 | .some(callback => moduleStat.callbackDeps 60 | .map(path => parse(path).name) 61 | .includes(callback)); 62 | const condition2 = () => 63 | // check if fragment is associated with a template 64 | // that references an include that references callback 65 | moduleStat.templateDeps 66 | .map(template => parse(template).base) 67 | .includes(m.data.template); 68 | const condition3 = () => 69 | // check if fragment references an include that references callback 70 | getAllIncludes(cheerio.load(mdToHtml(path, m.content))) 71 | .some(fragmentInclude => moduleStat.includeDeps 72 | .map(path => parse(path).base) 73 | .includes(fragmentInclude)); 74 | return condition1() || condition2() || condition3(); 75 | }); 76 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/getTemplateDeps.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Finds all the fragments that depend on a stale template file. 4 | */ 5 | 6 | const { parse } = require("path"); 7 | const { getCachedMatter } = require("../../../utils"); 8 | 9 | module.exports = (templateStat, pathsToAllFrags) => { 10 | templateStat.fragDeps = pathsToAllFrags 11 | .filter(path => getCachedMatter(path).data.template === parse(templateStat.path).base); 12 | }; -------------------------------------------------------------------------------- /lib/buildanalyzer/resolveDependencies/resolvers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | getTemplateDeps: require("./getTemplateDeps"), 4 | getIncludeDeps: require("./getIncludeDeps"), 5 | getCallbackDeps: require("./getCallbackDeps"), 6 | getRequiredModuleDeps: require("./getRequiredModuleDeps"), 7 | getDataDeps: require("./getDataDeps"), 8 | getAllIncludeDepsFromResolvedFragment: require("./getAllIncludeDepsFromResolvedFragment"), 9 | getFilterDeps: require("./getFilterDeps") 10 | }; -------------------------------------------------------------------------------- /lib/collections/filter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * () -> [{pageName, data}...] 4 | */ 5 | 6 | const { join } = require("path"); 7 | const importFresh = require("import-fresh"); 8 | const { filters } = require("../config"); 9 | const { log } = require("../utils"); 10 | 11 | module.exports = (collection, siteMetadata) => { 12 | try { 13 | return importFresh(join(process.cwd(), filters, collection.filterFn))({ collection, site: siteMetadata }); 14 | } catch (error) { 15 | log(error); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/collections/fragmentFactory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * () -> [fragments] 4 | */ 5 | 6 | const { join, parse } = require("path"); 7 | const { publicPathToUrl } = require("../utils"); 8 | const filter = require("./filter"); 9 | const { getPublicDestPath } = require("../metadata"); 10 | const validateDataset = require("./validateDataset"); 11 | const validateItem = require("./validateItem"); 12 | 13 | module.exports = (generator, siteMetadata) => { 14 | const frags = []; 15 | const collection = generator.matter.data.collection; 16 | const collectionDataset = filter(collection, siteMetadata); 17 | if (!validateDataset(collection.filterFn, collectionDataset)) { 18 | return []; 19 | } 20 | for (let index = 0; index < collectionDataset.length; index++) { 21 | const collectionDatasetItem = collectionDataset[index]; 22 | if (!validateItem(collection.filterFn, collectionDatasetItem, index)) { 23 | return []; 24 | } 25 | const fragment = JSON.parse(JSON.stringify(generator)); 26 | delete fragment.matter.data.collection; 27 | fragment.generator = generator.path; 28 | fragment.type = "fragment"; 29 | fragment.isStale = true; 30 | fragment.collection = { index, ...collectionDatasetItem }; 31 | // begining with v5.0.0 path.parse(collectionDatasetItem.pageName) is not called. 32 | fragment.path = join(parse(fragment.path).dir, collectionDatasetItem.pageName + parse(fragment.path).ext).toLowerCase(); 33 | fragment.destPath = getPublicDestPath(fragment); 34 | fragment.url = encodeURI(publicPathToUrl(fragment.destPath).toLowerCase()); 35 | frags.push(fragment); 36 | } 37 | for (const item of frags) { 38 | item.collection.totalItems = frags.length; 39 | } 40 | return frags; 41 | }; -------------------------------------------------------------------------------- /lib/collections/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * () -> [fragments] 4 | */ 5 | 6 | const fragmentFactory = require("./fragmentFactory"); 7 | 8 | module.exports = (generators, siteMetadata) => { 9 | const fragments = []; 10 | generators.forEach(generator => { 11 | fragments.push(...fragmentFactory(generator, siteMetadata)); 12 | }); 13 | return fragments; 14 | }; -------------------------------------------------------------------------------- /lib/collections/validateDataset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Validates the integrity of the dataset returned by the user's filterFn. 4 | * Returns true | false. 5 | * 6 | * Validations: 7 | * filterFn returned an array. 8 | * Each item in the dataset has at least 2 properties, and one of them is pagename. 9 | */ 10 | 11 | const { log } = require("../utils"); 12 | 13 | module.exports = (filterFnName, dataset) => { 14 | let valid = true; 15 | let msg = ""; 16 | if (typeof dataset === "undefined" || !Array.isArray(dataset)) { 17 | valid = false; 18 | msg = (`Error: Expected filterFn ${filterFnName} to return an Array, found ${typeof dataset}`); 19 | } else if (dataset.length === 0) { 20 | valid = false; 21 | msg = (`Error: filterFn ${filterFnName} returned an empty Array`); 22 | } 23 | if (!valid) { 24 | log(`Error: filterFn ${filterFnName} returned an invalid dataset:`); 25 | log(msg); 26 | return false; 27 | } 28 | return true; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/collections/validateItem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Validate a dataset item. 4 | * Note: Beginning with v3 the user must name the property that they assign their item's data to as "data". 5 | * Prior to v3 the user was able to name that property anything they wanted, but that made it impossible 6 | * to validate the item's integrity. 7 | */ 8 | 9 | const { log } = require("../utils"); 10 | 11 | module.exports = (filterFnName, collectionDatasetItem, index) => { 12 | const hasPageNameProperty = collectionDatasetItem.hasOwnProperty("pageName"); 13 | const hasDataProperty = collectionDatasetItem.hasOwnProperty("data"); 14 | const printError = missingPropertyName => 15 | log(`Error: Item number ${index} returned by filterFn ${filterFnName} is invalid - missing required property "${missingPropertyName}"`); 16 | !hasPageNameProperty && printError("pageName"); 17 | !hasDataProperty && printError("data"); 18 | return hasPageNameProperty && hasDataProperty; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/config/defaultOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | blogFolderName: "", 4 | baseUrl: "", 5 | sassSourceMaps: { 6 | development: true, 7 | release: true 8 | }, 9 | ignore: [] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/config/fileNames.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * A hash of special file names: 4 | * root/trio.manifest.json - contains the metadata that Trio exposes to JavaScript callbacks 5 | * root/.cache/meta.json - contains various bits of data used internally by Trio 6 | * root/.cache/stats.json - contains file system stats used for incremental builds 7 | * root/trio.json - user's project configuration file 8 | */ 9 | 10 | const { join } = require("path"); 11 | 12 | module.exports = { 13 | manifestFileName: join(process.cwd(), "trio.manifest.json"), 14 | metaFileName: join(process.cwd(), ".cache", "meta.json"), 15 | statsFileName: join(process.cwd(), ".cache", "stats.json"), 16 | userConfigFileName: join(process.cwd(), "trio.json"), 17 | cacheBustedLockFileName: join(process.cwd(), ".cache", "cachebustedLockFile"), 18 | busterManifestFileName: join(process.cwd(), "buster.manifest.json") 19 | }; -------------------------------------------------------------------------------- /lib/config/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Configuration 4 | * 5 | * Note: don't use `const { someUtility } = require("../utils")` 6 | * because requiring lib/utils/index.js will cause a circular 7 | * reference back to this module. 8 | */ 9 | 10 | const { join } = require("path"); 11 | 12 | const getTargetFolderName = () => process.env.TRIO_ENV_buildType === "release" && "release" || "public"; 13 | 14 | const defaultConfig = { 15 | frontMatterDelimiters: [""], 16 | frontMatterSeparator: "", 17 | jsFrontMatterDelimiters: ["/*", "*/"], 18 | target: "trio-fragment", 19 | cache: ".cache", 20 | source: "source", 21 | release: "release", 22 | targetFolder: getTargetFolderName(), 23 | sourceArticles: join("source", "fragments", "articles"), 24 | sourceMedia: join("source", "media"), 25 | publicMedia: join(getTargetFolderName(), "media"), 26 | sourceCss: join("source", "css"), 27 | publicCss: join(getTargetFolderName(), "css"), 28 | sourceScripts: join("source", "scripts"), 29 | publicScripts: join(getTargetFolderName(), "scripts"), 30 | sourceEtc: join("source", "etc"), 31 | publicEtc: join(getTargetFolderName()), 32 | sourceData: join("source", "data"), 33 | fragments: join("source", "fragments"), 34 | includes: join("source", "includes"), 35 | callbacks: join("source", "callbacks"), 36 | filters: join("source", "filters"), 37 | articleCallback: "article.js", 38 | templates: join("source", "templates") 39 | }; 40 | 41 | module.exports = defaultConfig; 42 | -------------------------------------------------------------------------------- /lib/config/permalinks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Extract permalinks from the trio.json config file. 4 | * 5 | * Permanlinks are defined as follows: 6 | * "[target-folder][, ...]: permalink" 7 | * 8 | * Note: One or more target folders can be defined for a single permalink. 9 | */ 10 | 11 | module.exports = (permalinks) => { 12 | const result = []; 13 | permalinks && permalinks.forEach(permalink => { 14 | const pl = permalink.split(":"); 15 | // create a permalink for each target folder 16 | const targetFolders = pl[0].split(","); 17 | targetFolders.forEach(targetFolder => { 18 | result.push({ targetFolder: targetFolder.trim(), path: pl[1].trim() }); 19 | }); 20 | }); 21 | return result; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/config/userConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { readCache } = require("../utils"); 3 | const defaultOptions = require("../config/defaultOptions"); 4 | const { userConfigFileName } = require("../config/fileNames"); 5 | const getPermalinks = require("../config/permalinks"); 6 | 7 | module.exports = () => { 8 | const userConfig = readCache(userConfigFileName); 9 | userConfig.permalinks = getPermalinks(userConfig.permalinks); 10 | userConfig.baseUrl = userConfig.baseUrl || ""; 11 | userConfig.ignore = Array.isArray(userConfig.ignore) && userConfig.ignore || typeof userConfig.ignore === "string" && [userConfig.ignore] || []; 12 | process.env.TRIO_ENV_baseUrl = userConfig.baseUrl; 13 | return { ...defaultOptions, ...userConfig }; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/config/validatePermalinks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Validates that permalinks declared in trio.json are valid, specifically that 4 | * their target folders match a stat's path property. If none do then issue a 5 | * warning message. 6 | */ 7 | 8 | const { join } = require("path"); 9 | const config = require("../config"); 10 | const { log, warn } = require("../utils"); 11 | 12 | module.exports = (stats) => { 13 | const reportMissingTargetFolders = missingTargetPaths => { 14 | warn("Warning: The following permalink target folders do not exist:"); 15 | missingTargetPaths.forEach((missingTargetFolder, i) => { 16 | log(`${i + 1}) ${missingTargetFolder}`); 17 | }); 18 | }; 19 | 20 | const missingTargetPaths = []; 21 | config.userConfig.permalinks.forEach(({ targetFolder }) => { 22 | const tf = join(config.fragments, targetFolder); 23 | const found = stats.some(stat => { 24 | return stat.type === "fragment" && stat.path.startsWith(tf); 25 | }); 26 | !found && missingTargetPaths.push(tf); 27 | }); 28 | missingTargetPaths.length > 0 && reportMissingTargetFolders(missingTargetPaths); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/generator/blog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { 3 | createSortedTagCatalog, 4 | createSortedArticlesCatalog, 5 | createRelatedArticlesByTag, 6 | createRelatedArticlesByTagFlattened, 7 | createRelatedArticlesByCategory, 8 | createSortedCategoriesCatalog 9 | } = require("../metadata"); 10 | const { 11 | articlesHaveTags, 12 | articlesHaveCategories, 13 | metadataHasArticles 14 | } = require("../utils"); 15 | 16 | module.exports = siteMetadata => { 17 | if (metadataHasArticles(siteMetadata.frags)) { 18 | siteMetadata.articlesCatalog = 19 | createSortedArticlesCatalog(siteMetadata.frags); 20 | siteMetadata.articlesCount = siteMetadata.articlesCatalog.length; 21 | if (articlesHaveTags(siteMetadata.articlesCatalog)) { 22 | siteMetadata.tagsCatalog = 23 | createSortedTagCatalog(siteMetadata.articlesCatalog); 24 | createRelatedArticlesByTag(siteMetadata.articlesCatalog); 25 | createRelatedArticlesByTagFlattened(siteMetadata.articlesCatalog); 26 | } 27 | if (articlesHaveCategories(siteMetadata.articlesCatalog)) { 28 | siteMetadata.categoriesCatalog = 29 | createSortedCategoriesCatalog(siteMetadata.articlesCatalog); 30 | createRelatedArticlesByCategory(siteMetadata.articlesCatalog); 31 | } 32 | } 33 | }; -------------------------------------------------------------------------------- /lib/generator/cleanPublic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Cleans public folder by deleting stale HTML pages and 4 | * recursively deleting folders by traversing up through 5 | * the file's path for empty folders. 6 | */ 7 | 8 | const { rmdirSync, statSync, unlinkSync } = require("fs-extra"); 9 | const { dirname } = require("path"); 10 | const { targetFolder } = require("../config"); 11 | const { globFriendly, log } = require("../utils"); 12 | 13 | const isPathEmpty = path => globFriendly(`${path}/**/*.*`).length === 0; 14 | 15 | const cleanRecursive = path => { 16 | // delete the file or folder 17 | if (statSync(path).isDirectory()) { 18 | rmdirSync(path); 19 | } else if (statSync(path).isFile()) { 20 | unlinkSync(path); 21 | } 22 | const newPath = dirname(path); 23 | // recursively iterate over parent folder until the parent folder isn't empty 24 | if (newPath !== targetFolder && isPathEmpty(newPath)) { 25 | return cleanRecursive(newPath); 26 | } 27 | }; 28 | 29 | module.exports = pagesForGarbageCollection => { 30 | pagesForGarbageCollection.forEach(path => { 31 | try { 32 | // delete empty ancestor folders 33 | cleanRecursive(path); 34 | } catch (error) { 35 | log(`Error: An error ocurred while cleaning public folder for file ${path}`); 36 | log(error); 37 | } 38 | }); 39 | }; -------------------------------------------------------------------------------- /lib/generator/copyOtherFiles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fs = require("fs-extra"); 3 | const config = require("../config"); 4 | const { log } = require("../utils"); 5 | 6 | module.exports = () => { 7 | try { 8 | // copy source/etc to public 9 | fs.pathExistsSync(config.sourceEtc) && fs.copySync(config.sourceEtc, config.publicEtc); 10 | 11 | // copy source/css to public/css 12 | fs.pathExistsSync(config.sourceCss) && fs.copySync(config.sourceCss, config.publicCss); 13 | 14 | // copy source/scripts to public/scripts 15 | fs.pathExistsSync(config.sourceScripts) && fs.copySync(config.sourceScripts, config.publicScripts); 16 | 17 | // copy source/media to public/media 18 | fs.pathExistsSync(config.sourceMedia) && fs.copySync(config.sourceMedia, config.publicMedia); 19 | } catch (error) { 20 | log(error); 21 | } 22 | }; -------------------------------------------------------------------------------- /lib/generator/createManifest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { writeJSONSync } = require("fs-extra"); 3 | const config = require("../config"); 4 | const { manifestFileName } = require("../config/fileNames"); 5 | 6 | const articleFilter = frags => 7 | frags.filter(frag => !frag.path.startsWith(config.sourceArticles)); 8 | 9 | module.exports = siteMetadata => { 10 | const manifest = {}; 11 | manifest.timestamp = siteMetadata.timestamp; 12 | manifest.version = siteMetadata.version; 13 | manifest.buildType = siteMetadata.buildType; 14 | manifest.userConfig = siteMetadata.userConfig; 15 | manifest.frags = articleFilter(siteMetadata.frags); 16 | manifest.wipsCount = siteMetadata.wipsCount; 17 | manifest.wips = siteMetadata.wips; 18 | manifest.articlesCount = siteMetadata.articlesCount; 19 | manifest.articlesCatalog = siteMetadata.articlesCatalog; 20 | manifest.tagsCatalog = siteMetadata.tagsCatalog; 21 | manifest.categoriesCatalog = siteMetadata.categoriesCatalog; 22 | writeJSONSync(manifestFileName, manifest, { spaces: " " }); 23 | }; -------------------------------------------------------------------------------- /lib/generator/garbageCollector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { statsFileName } = require("../config/fileNames"); 3 | const { doesCachedStatsFileExists, readCache } = require("../utils"); 4 | 5 | module.exports = (currentStats) => { 6 | const cachedStatsFile = doesCachedStatsFileExists() && 7 | readCache(statsFileName).filter(stat => stat.type === "fragment") || []; 8 | 9 | const currentStatsMap = new Map(); 10 | currentStats.forEach(stat => currentStatsMap.set(stat.path, stat)); 11 | 12 | const diffs = cachedStatsFile.filter(stat => 13 | !(currentStatsMap.has(stat.path) && 14 | currentStatsMap.get(stat.path).destPath === stat.destPath)); 15 | 16 | const cachedStatsMap = diffs && new Map(); 17 | cachedStatsMap && cachedStatsFile 18 | .forEach(stat => cachedStatsMap.set(stat.path, stat)); 19 | 20 | const pagesForGarbageCollection = cachedStatsMap && cachedStatsMap.size > 0 && 21 | diffs.reduce((accum, diff) => { 22 | if (diff.type === "fragment") { 23 | accum.push(diff.destPath); 24 | } else { 25 | cachedStatsMap 26 | .get(diff.path).fragDeps 27 | .forEach(diffStatFragDep => 28 | accum.push(cachedStatsMap.get(diffStatFragDep).destPath)); 29 | } 30 | return accum; 31 | }, []) || []; 32 | return pagesForGarbageCollection; 33 | }; -------------------------------------------------------------------------------- /lib/generator/getAssets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Returns an object whose properties represent the project's assets, including wip details. 4 | * If running a release build, will filter out all wip fragments. 5 | */ 6 | 7 | const { parse } = require("path"); 8 | const config = require("../config"); 9 | const { getIgnoredSourceFolders, globFriendly, isFile } = require("../utils"); 10 | 11 | module.exports = () => { 12 | const isWIP = path => { 13 | const parseInfo = parse(path); 14 | return parseInfo.dir.startsWith(config.fragments) && parseInfo.name.startsWith("_"); 15 | }; 16 | 17 | // get ignored folders 18 | const ignore = ["**/*.DS_Store", ...getIgnoredSourceFolders("/")]; 19 | 20 | // get paths to all project assets (generators, fragments, templates, includes, callbacks, .etc) 21 | const paths = globFriendly(`${config.source}/**/*`, 22 | { dot: true, ignore }) 23 | .filter(asset => isFile(asset)); 24 | 25 | const assets = []; 26 | const wips = []; 27 | 28 | paths.forEach(path => { 29 | const wip = isWIP(path); 30 | // collect wips 31 | wip && wips.push(path); 32 | // collect assets but don't include wips if release 33 | ((process.env.TRIO_ENV_buildType === "release" && !wip) || 34 | (process.env.TRIO_ENV_buildType !== "release")) && assets.push(path); 35 | }); 36 | 37 | return { assets, wips }; 38 | }; -------------------------------------------------------------------------------- /lib/generator/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { existsSync } = require("fs-extra"); 3 | const { integrityCheck, resolveDependencies, fragmentFrontMatterCheck } = require("../buildanalyzer"); 4 | const config = require("../config"); 5 | const validatePermalinks = require("../config/validatePermalinks"); 6 | const { metaFileName, statsFileName } = require("../config/fileNames"); 7 | const getAssets = require("./getAssets"); 8 | const reportWips = require("./reportWips"); 9 | const reportMissingAssets = require("./reportMissingAssets"); 10 | const makePublicFolder = require("./makePublicFolder"); 11 | const mashup = require("./mashup"); 12 | const sassRender = require("./sassRender"); 13 | const createManifest = require("./createManifest"); 14 | const createBlogMetadata = require("./blog"); 15 | const cleanPublic = require("./cleanPublic"); 16 | const copyOtherFiles = require("./copyOtherFiles"); 17 | const prepareForReleaseBuild = require("./prepareForReleaseBuild"); 18 | const garbageCollector = require("./garbageCollector"); 19 | const { 20 | getFragmentMetadata, 21 | getAllIncludesMetadata, 22 | getAllData 23 | } = require("../metadata"); 24 | const { 25 | clearReadCache, 26 | clearMatterCache, 27 | doesCachedStatsFileExists, 28 | isPlural, 29 | log, 30 | readCache, 31 | triggerOneOffBuild, 32 | writeCachedStatsFile, 33 | writeCachedMetaFile 34 | } = require("../utils"); 35 | const metrics = require("../metrics"); 36 | const getStats = require("../stats/getStats"); 37 | const collections = require("../collections"); 38 | const userConfig = require("../config/userConfig"); 39 | 40 | module.exports = async () => { 41 | metrics.clearTimers(); 42 | metrics.startTimer("total elapsed time"); 43 | 44 | // clear read caches 45 | clearReadCache(); 46 | clearMatterCache(); 47 | 48 | // get Trio's internal metadata 49 | const cacheMetadata = existsSync(metaFileName) && 50 | readCache(metaFileName) || { lastTrioVersionUsed: "" }; 51 | 52 | // trigger a one off build if Trio's version has changed 53 | cacheMetadata.lastTrioVersionUsed !== process.env.TRIO_ENV_version && 54 | triggerOneOffBuild(); 55 | 56 | config.userConfig = userConfig(); 57 | 58 | const siteMetadata = {}; 59 | siteMetadata.timestamp = new Date().toLocaleString(); 60 | siteMetadata.version = process.env.TRIO_ENV_version; 61 | siteMetadata.buildType = process.env.TRIO_ENV_buildType; 62 | siteMetadata.userConfig = config.userConfig; 63 | siteMetadata.dataCatalog = getAllData(); 64 | 65 | const publicFolderExists = existsSync(config.targetFolder); 66 | 67 | process.env.TRIO_ENV_buildType === "release" && await prepareForReleaseBuild(); 68 | 69 | if (process.env.TRIO_ENV_buildType !== "release" && 70 | process.env.TRIO_ENV_buildIncrementally === 71 | "no-incremental-build" || !publicFolderExists) { 72 | makePublicFolder(); 73 | } 74 | 75 | config.userConfig.baseUrl !== "" && log(`baseUrl is "${config.userConfig.baseUrl}"`); 76 | 77 | // get project assets, including wips 78 | metrics.startTimer("getting assets"); 79 | const { assets, wips } = getAssets(); 80 | siteMetadata.wipsCount = wips.length; 81 | siteMetadata.wips = wips; 82 | reportWips(wips); 83 | metrics.stopTimer("getting assets"); 84 | 85 | // integrity check the project 86 | metrics.startTimer("checking project integrity"); 87 | fragmentFrontMatterCheck(assets); 88 | const missingAssets = integrityCheck(assets); 89 | if (missingAssets && missingAssets.length > 0) { 90 | reportMissingAssets(missingAssets); 91 | } 92 | metrics.stopTimer("checking project integrity"); 93 | 94 | // generate stats 95 | metrics.startTimer("generating stats"); 96 | let stats = getStats(assets); 97 | metrics.stopTimer("generating stats"); 98 | 99 | // resolve dependencies 100 | metrics.startTimer("resolving dependencies for stats"); 101 | const resolvedDependencies = resolveDependencies(stats); 102 | metrics.stopTimer("resolving dependencies for stats"); 103 | 104 | // generate metadata 105 | metrics.startTimer("generating metadata for stale stat fragments"); 106 | getFragmentMetadata(resolvedDependencies.fragmentStats); 107 | metrics.stopTimer("generating metadata for stale stat fragments"); 108 | 109 | // when building incrementally, we need to merge the old 110 | // stat file's content with the new stats 111 | metrics.startTimer("reconciling stats"); 112 | if (process.env.TRIO_ENV_buildIncrementally === "incremental-build" && doesCachedStatsFileExists()) { 113 | const oldStats = readCache(statsFileName); 114 | const oldStatsMap = new Map(oldStats.map(oldStat => [oldStat.path, oldStat])); 115 | const newStatsMap = new Map(stats.map(stat => [stat.path, stat])); 116 | oldStatsMap.forEach((oldStat, oldStatPath) => { 117 | // generators should always be marked as "alwaysBuild" so there is no 118 | // need to carry over generated collection stats to the new stats file 119 | if (!oldStat.generator && existsSync(oldStatPath) && newStatsMap.get(oldStatPath).isStale === false) { 120 | oldStat.isStale = false; 121 | newStatsMap.set(oldStatPath, oldStat); 122 | } 123 | }); 124 | stats = [...newStatsMap.values()]; 125 | } 126 | // add all stat fragments to siteMetadata.frags 127 | siteMetadata.frags = stats.filter(stat => stat.type === "fragment" || stat.type === "generator"); 128 | metrics.stopTimer("reconciling stats"); 129 | 130 | metrics.startTimer("creating blog metadata"); 131 | createBlogMetadata(siteMetadata); 132 | metrics.stopTimer("creating blog metadata"); 133 | 134 | // create stats for collections 135 | metrics.startTimer("processing collections"); 136 | const collectionFragments = collections(resolvedDependencies.fragmentStats 137 | .filter(stat => stat.type === "generator"), siteMetadata); 138 | collectionFragments && stats.push(...collectionFragments); 139 | collectionFragments && resolvedDependencies.fragmentStats.push(...collectionFragments); 140 | siteMetadata.frags.push(...collectionFragments); 141 | // remove all generators from the list of resolved fragments - they 142 | // have done their job and are not needed for page generation 143 | resolvedDependencies.fragmentStats = resolvedDependencies.fragmentStats.length > 0 144 | ? resolvedDependencies.fragmentStats 145 | .filter(resolvedFragment => resolvedFragment.type !== "generator") 146 | : resolvedDependencies.fragmentStats; 147 | const rfl = resolvedDependencies.fragmentStats.length; 148 | log(`generating: ${rfl} ${isPlural(rfl) ? "pages" : "page"}`); 149 | metrics.stopTimer("processing collections"); 150 | 151 | // set default values if needed in siteMetadata 152 | siteMetadata.frags = siteMetadata.frags || []; 153 | siteMetadata.wipsCount = siteMetadata.wipsCount || 0; 154 | siteMetadata.wips = siteMetadata.wips || []; 155 | siteMetadata.articlesCount = siteMetadata.articlesCount || 0; 156 | siteMetadata.articlesCatalog = siteMetadata.articlesCatalog || []; 157 | siteMetadata.tagsCatalog = siteMetadata.tagsCatalog || []; 158 | siteMetadata.categoriesCatalog = siteMetadata.categoriesCatalog || []; 159 | 160 | // validate permalinks against the stats 161 | metrics.startTimer("validating permlinks"); 162 | validatePermalinks(stats); 163 | metrics.stopTimer("validating permlinks"); 164 | 165 | // generate pages from asset based metadata 166 | metrics.startTimer("composition"); 167 | const includesMetadataMap = new Map(); 168 | getAllIncludesMetadata(resolvedDependencies.includeStats) 169 | .forEach(imd => includesMetadataMap.set(imd.path, imd)); 170 | if (resolvedDependencies.fragmentStats.length > 0) { 171 | const fragsMap = new Map(); 172 | siteMetadata.frags.forEach(frag => 173 | fragsMap.set(frag.path, frag)); 174 | for (const fragmentStat of resolvedDependencies.fragmentStats) { 175 | await mashup(fragsMap.get(fragmentStat.path), includesMetadataMap, siteMetadata); 176 | } 177 | } 178 | metrics.stopTimer("composition"); 179 | 180 | // create trio.manifest.json 181 | process.env.TRIO_ENV_buildType !== "release" && 182 | createManifest(siteMetadata); 183 | 184 | // clean public folder 185 | if (process.env.TRIO_ENV_buildIncrementally === "incremental-build") { 186 | metrics.startTimer("cleaning public folder"); 187 | const pagesForGarbageCollection = garbageCollector(siteMetadata.frags); 188 | cleanPublic(pagesForGarbageCollection); 189 | metrics.stopTimer("cleaning public folder"); 190 | } 191 | 192 | copyOtherFiles(); 193 | await sassRender(); 194 | 195 | // save current project stats to .cache 196 | process.env.TRIO_ENV_buildType !== "release" && 197 | writeCachedStatsFile(stats); 198 | 199 | // save Trio's current internal metadata 200 | cacheMetadata.lastTrioVersionUsed = process.env.TRIO_ENV_version; 201 | writeCachedMetaFile(cacheMetadata); 202 | 203 | log(metrics.stopTimer("total elapsed time")); 204 | metrics.deleteTimer("total elapsed time"); 205 | 206 | // print metrics 207 | process.env.TRIO_ENV_printMetrics === "print-metrics" && metrics.forEach(timer => log(timer.elapsed)); 208 | }; -------------------------------------------------------------------------------- /lib/generator/makePublicFolder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { 3 | ensureDirSync, 4 | removeSync 5 | // copySync, 6 | // pathExistsSync 7 | } = require("fs-extra"); 8 | const config = require("../config"); 9 | const { log } = require("../utils"); 10 | 11 | module.exports = () => { 12 | try { 13 | removeSync(config.targetFolder); 14 | ensureDirSync(config.publicCss); 15 | ensureDirSync(config.publicMedia); 16 | ensureDirSync(config.publicScripts); 17 | } catch (error) { 18 | log(error); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/generator/mashup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Creates a composite from a template, its associated fragment, 4 | * and all the includes that are declared in both. 5 | */ 6 | 7 | const { join, parse } = require("path"); 8 | const cheerio = require("cheerio"); 9 | const { 10 | callTagCallbacks, 11 | getAllIncludes, 12 | getTagCallbacks, 13 | readCache 14 | } = require("../utils"); 15 | const save = require("./save"); 16 | const config = require("../config"); 17 | 18 | // When building for development those tags that are replaced by their content 19 | // are commented out instead of removed so that they are preserved for viewing. 20 | // When building for release those tags that are replaced by their content 21 | // are removed. 22 | const commentAndRemoveTargetTag = (selector, $) => { 23 | const $selector = $(selector); 24 | const contents = $selector.html(); 25 | $selector.empty(); 26 | $selector.after(contents); 27 | process.env.TRIO_ENV_buildType !== "release" && 28 | $selector.before(``); 29 | $selector.remove(); 30 | }; 31 | 32 | const mergeContent = ($, selector, asset) => { 33 | $(selector).append(asset.matter.content); 34 | }; 35 | 36 | // return true if the data-trio-include's value actually names the file and false if it's 37 | // a reference to a property in the asset's front matter whose value is the name of the file 38 | const isIncludeAFile = include => !!parse(include).ext; 39 | 40 | const mergeIncludes = ($, frag, includesMetadataMap, includes) => { 41 | includes.forEach(include => { 42 | const includeFileName = isIncludeAFile(include) ? include : frag.matter.data[include]; 43 | const includeMetadata = includesMetadataMap.get(join(config.includes, includeFileName)); 44 | mergeContent($, `[data-trio-include='${include}']`, includeMetadata); 45 | }); 46 | }; 47 | 48 | module.exports = async (frag, includesMetadataMap, siteMetadata) => { 49 | const $ = cheerio.load(readCache(frag.matter.data.template)); 50 | mergeContent($, `[data-${config.target}='']`, frag); 51 | // get the values from all the "data-trio-include" attributes in the composite 52 | const includes = getAllIncludes($); 53 | mergeIncludes($, frag, includesMetadataMap, includes); 54 | $("title").text(frag.matter.data.title); 55 | const tagCallbacksMetadata = getTagCallbacks($); 56 | for (const tagCallbackMetadata of tagCallbacksMetadata) { 57 | await callTagCallbacks( 58 | tagCallbackMetadata.$tag, 59 | tagCallbackMetadata.callback, 60 | $, 61 | frag, 62 | siteMetadata 63 | ); 64 | } 65 | // now that all the callbacks have been run for the composite it is safe to comment and remove those tags that aren't appended to 66 | !frag.matter.data.appendToTarget && commentAndRemoveTargetTag("data-trio-fragment", $); 67 | includes.forEach(include => { 68 | const includeFileName = isIncludeAFile(include) ? include : frag.matter.data[include]; 69 | const includeMetadata = includesMetadataMap.get(join(config.includes, includeFileName)); 70 | !includeMetadata.matter || includeMetadata.matter.data.appendToTarget === false && commentAndRemoveTargetTag(`[data-trio-include='${include}']`, $); 71 | }); 72 | save($, frag.destPath); 73 | }; 74 | -------------------------------------------------------------------------------- /lib/generator/prefixUrls.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const config = require("../config"); 3 | 4 | const getAllTrioLinks = $ => { 5 | const els = []; 6 | $("[data-trio-link]").each((index, element) => { 7 | els.push(element); 8 | }); 9 | return els; 10 | }; 11 | 12 | const prefixPath = path => path === "/" && 13 | config.userConfig.baseUrl || 14 | config.userConfig.baseUrl + path; 15 | 16 | module.exports = $ => { 17 | // we only care about absolute urls 18 | // if it starts with an "/" it is considered absolute 19 | const isPathAbsolute = el => ["href", "src"].some(type => 20 | $(el).attr(type) && 21 | $(el).attr(type).length && 22 | $(el).attr(type)[0] === "/" 23 | ); 24 | 25 | getAllTrioLinks($).filter(isPathAbsolute).forEach(el => { 26 | const $el = $(el); 27 | const attr = $(el).attr("href") && "href" || "src"; 28 | const path = prefixPath($el.attr(attr)); 29 | $el.attr(attr, path); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/generator/prepareForReleaseBuild.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { existsSync, remove, unlink } = require("fs-extra"); 3 | const config = require("../config"); 4 | const fileNames = require("../config/fileNames"); 5 | 6 | module.exports = async () => { 7 | await remove(`${config.release}`); 8 | existsSync(fileNames.cacheBustedLockFileName) && await unlink(fileNames.cacheBustedLockFileName); 9 | existsSync(fileNames.busterManifestFileName) && await unlink(fileNames.busterManifestFileName); 10 | }; -------------------------------------------------------------------------------- /lib/generator/removeComments.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = $ => { 3 | $("*").contents().each(function () { 4 | if (this.nodeType === 8) { 5 | $(this).remove(); 6 | } 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/generator/removeDataAttributes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = $ => { 3 | $("*").each((i, el) => { 4 | const $el = $(el); 5 | const elem = $(el).get(0); 6 | Object.keys(elem.attribs) 7 | .filter(key => key.startsWith("data-trio")) 8 | .forEach(key => $el.removeAttr(key)); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/generator/reportMissingAssets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { log, warn } = require("../utils"); 3 | 4 | module.exports = missingAssets => { 5 | warn("Warning: The following referenced assets can't be found:"); 6 | const maSet = new Set(missingAssets); 7 | const a = Array.from(maSet); 8 | a.forEach((missingAsset, i) => { 9 | log(`${i + 1}) ${missingAsset}`); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/generator/reportWips.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Logs wips information to the console. 4 | */ 5 | 6 | const { log, warn } = require("../utils"); 7 | 8 | module.exports = (wips, siteMetadata) => { 9 | wips.length > 0 && log(`Found ${wips.length} wips:`); 10 | wips.length > 0 && wips.forEach(wip => 11 | warn(process.env.TRIO_ENV_buildType === "release" 12 | ? ` Warning: Ignoring ${wip}` 13 | : ` ${wip}`) 14 | ); 15 | }; -------------------------------------------------------------------------------- /lib/generator/sassRender.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const sass = require("sass"); 3 | const { join, parse } = require("path"); 4 | const { writeFileSync } = require("fs-extra"); 5 | const postcss = require("postcss"); 6 | const autoprefixer = require("autoprefixer"); 7 | const cssnano = require("cssnano")({ preset: "cssnano-preset-default" }); 8 | const config = require("../config"); 9 | const { globFriendly, warn } = require("../utils"); 10 | 11 | module.exports = async () => { 12 | const mainSassFile = globFriendly("source/sass/{*.scss,*.sass}", { 13 | ignore: "source/sass/_*.*" 14 | })[0]; 15 | if (mainSassFile) { 16 | const generateSourceMap = process.env.TRIO_ENV_buildType === "release" && 17 | config.userConfig.sassSourceMaps.release || 18 | process.env.TRIO_ENV_buildType !== "release" && 19 | config.userConfig.sassSourceMaps.development; 20 | const sassOutputFileName = join(config.publicCss, `${parse(mainSassFile).name}.css`); 21 | const sassMapFileName = join(config.publicCss, `${parse(mainSassFile).name}.css.map`); 22 | const sassConfig = { 23 | file: mainSassFile, 24 | outFile: sassOutputFileName, 25 | sourceMap: generateSourceMap 26 | }; 27 | const postcssPlugins = process.env.TRIO_ENV_buildType !== "release" 28 | ? [autoprefixer] 29 | : [autoprefixer, cssnano]; 30 | sassConfig.sourceMapContents = generateSourceMap && true; 31 | const sassResult = sass.renderSync(sassConfig); 32 | const postcssConfig = generateSourceMap 33 | // Note to self: your must use "prev:" and set it to the map produced by node-sass otherwise it won't work! 34 | ? { from: sassOutputFileName, to: sassOutputFileName, map: { prev: sassResult.map.toString(), inline: false } } 35 | : { from: undefined }; 36 | const postcssResult = await postcss(postcssPlugins).process(sassResult.css.toString(), postcssConfig); 37 | postcssResult.warnings().forEach(warning => warn(warning.toString())); 38 | const cssOutput = generateSourceMap && process.env.TRIO_ENV_buildType === "release" && 39 | postcssResult.css.replace(`/*# sourceMappingURL=${parse(mainSassFile).name}.css.map */`, 40 | `/*# sourceMappingURL=/css/${parse(mainSassFile).name}.css.map */`) || postcssResult.css; 41 | generateSourceMap && writeFileSync(sassMapFileName, postcssResult.map.toString()); 42 | writeFileSync(sassOutputFileName, cssOutput); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/generator/save.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const beautify = require("js-beautify").html; 3 | const { outputFileSync } = require("fs-extra"); 4 | const prefixUrls = require("./prefixUrls"); 5 | const removeDataAttributes = require("./removeDataAttributes"); 6 | const removeComments = require("./removeComments"); 7 | 8 | module.exports = ($, destPath) => { 9 | prefixUrls($); 10 | if (process.env.TRIO_ENV_buildType === "release") { 11 | removeDataAttributes($); 12 | removeComments($); 13 | } 14 | outputFileSync(destPath, beautify($.html(), { 15 | preserve_newlines: true, 16 | max_preserve_newlines: 1 17 | })); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/metadata/createRelatedArticlesByCategory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const createRelatedItem = require("../utils/createRelatedItem"); 3 | 4 | const isCategoryRelated = (c, cc) => 5 | c.length === cc.length && c.join("/") === cc.join("/"); 6 | 7 | module.exports = articles => { 8 | articles.forEach((article, i, a) => { 9 | if (article.matter.data.category && article.matter.data.category.length) { 10 | article.relatedArticlesByCategory = { category: article.matter.data.category.join("/"), related: [] }; 11 | a.forEach(item => { 12 | if (item !== article && isCategoryRelated(article.matter.data.category, item.matter.data.category)) { 13 | article.relatedArticlesByCategory.related.push(createRelatedItem(item)); 14 | } 15 | }); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/metadata/createRelatedArticlesByTag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const createRelatedItem = require("../utils/createRelatedItem"); 3 | 4 | module.exports = articles => { 5 | articles.forEach((article, i, a) => { 6 | const matches = []; 7 | article.matter.data.tag.forEach(tag => { 8 | matches.push({ tag, related: [] }); 9 | a.forEach(smd => { 10 | if (article.url !== smd.url && smd.matter.data.tag.includes(tag)) { 11 | matches[matches.length - 1].related.push(createRelatedItem(smd)); 12 | } 13 | }); 14 | }); 15 | article.relatedArticlesByTag = matches; 16 | }); 17 | }; -------------------------------------------------------------------------------- /lib/metadata/createRelatedArticlesByTagFlattened.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { createRelatedItem, sortArticles } = require("../utils"); 3 | 4 | module.exports = (articles) => { 5 | // related articles list 6 | articles.forEach(article => { 7 | const relatedArticlesSet = new Set(); 8 | article.relatedArticlesByTag.forEach(item => { 9 | item.related.forEach(related => 10 | relatedArticlesSet.add(`${related.articleDate}\n${related.url}\n${related.title}\n${related.excerpt}`)); 11 | }); 12 | article.relatedArticlesByTagFlattened = Array.from(relatedArticlesSet) 13 | .sort(sortArticles) 14 | .map(item => createRelatedItem(item)); 15 | }); 16 | }; -------------------------------------------------------------------------------- /lib/metadata/createSortedArticlesCatalog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { sep } = require("path"); 3 | const config = require("../config"); 4 | 5 | // 2018-01-01-name.ext -> 20180101 6 | const convertFilePathToDate = filePath => { 7 | const i = filePath.search(/\d{4}-\d{2}-\d{2}/); 8 | if (!i) { 9 | throw new Error(`Error: Invalid article file name ${filePath}`); 10 | } 11 | return parseInt(filePath.substring(i, i + 10).split("-").join(""), 10); 12 | }; 13 | 14 | const sort = (a, b) => { 15 | const n1 = convertFilePathToDate(a.path); 16 | const n2 = convertFilePathToDate(b.path); 17 | const result = n2 - n1; 18 | return result !== 0 19 | ? result : a.matter.data.title === b.matter.data.title 20 | ? 0 : a.matter.data.title < b.matter.data.title 21 | ? -1 : 1; 22 | }; 23 | 24 | const linkArticles = articles => { 25 | articles.forEach((article, index, array) => { 26 | article.nextArticleUrl = index === 0 ? "" : array[index - 1].url; 27 | article.previousArticleUrl = index === array.length - 1 ? "" : array[index + 1].url; 28 | }); 29 | }; 30 | 31 | module.exports = frags => { 32 | const articles = 33 | frags.filter(frag => frag.path.startsWith(`${config.sourceArticles}`) && 34 | frag.path !== `${config.sourceArticles}${sep}index.html`); 35 | articles.sort(sort); 36 | linkArticles(articles); 37 | return articles; 38 | }; -------------------------------------------------------------------------------- /lib/metadata/createSortedCategoriesCatalog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { createRelatedItem } = require("../utils"); 3 | 4 | const addArticleToRelated = (related, article) => 5 | related.push(createRelatedItem(article)); 6 | 7 | const sortCategories = (a, b) => a.category === b.category 8 | ? 0 : a.category < b.category ? -1 : +1; 9 | 10 | module.exports = articlesCatalog => { 11 | const catalog = []; 12 | articlesCatalog.filter(article => article.matter.data.category && article.matter.data.category.length) 13 | .forEach(article => { 14 | const catItem = catalog.find(item => item.category === article.matter.data.category.join("/")); 15 | if (catItem) { 16 | addArticleToRelated(catItem.related, article); 17 | } else { 18 | catalog.push({ category: article.matter.data.category.join("/"), related: [] }); 19 | addArticleToRelated(catalog[catalog.length - 1].related, article); 20 | } 21 | }); 22 | return catalog.sort(sortCategories); 23 | }; -------------------------------------------------------------------------------- /lib/metadata/createSortedTagCatalog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { createRelatedItem } = require("../utils"); 3 | 4 | const sortTag = (a, b) => a.tag === b.tag 5 | ? 0 : a.tag < b.tag 6 | ? -1 : 1; 7 | 8 | module.exports = frags => { 9 | const catalog = []; 10 | frags.filter(frag => frag.matter.data.tag.length) 11 | .forEach(frag => { 12 | frag.matter.data.tag.forEach(tag => { 13 | const found = catalog.find(cat => cat.tag === tag); 14 | if (!found) { 15 | catalog.push({ 16 | tag, 17 | related: [createRelatedItem(frag)] 18 | }); 19 | } else { 20 | found.related.push(createRelatedItem(frag)); 21 | } 22 | }); 23 | }); 24 | catalog.sort(sortTag); 25 | return catalog; 26 | }; -------------------------------------------------------------------------------- /lib/metadata/getAllData.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { parse } = require("path"); 3 | const { globFriendly, readCache } = require("../utils"); 4 | 5 | module.exports = () => { 6 | return globFriendly("source/data/**/*.json") 7 | .reduce((accum, path) => { 8 | const propertyName = parse(path).name; 9 | return { ...accum, ...{ [propertyName]: readCache(path) } }; 10 | }, {}); 11 | }; -------------------------------------------------------------------------------- /lib/metadata/getAllIncludesMetadata.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { getCachedMatter, mdToHtml } = require("../utils"); 3 | 4 | const commonMetadata = path => { 5 | const metadata = {}; 6 | metadata.path = path; 7 | metadata.matter = getCachedMatter(path); 8 | metadata.matter.data.appendToTarget = metadata.matter.data.appendToTarget || false; 9 | metadata.matter.content = mdToHtml(metadata.path, metadata.matter.content); 10 | return metadata; 11 | }; 12 | 13 | module.exports = includeStats => { 14 | return includeStats.map(includeStat => { 15 | return commonMetadata(includeStat.path); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/metadata/getArticleDateFromPath.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Returns the article's date derived from its path 4 | */ 5 | 6 | module.exports = fragPath => { 7 | const i = fragPath.search(/\d{4}-\d{2}-\d{2}/); 8 | return fragPath.substring(i, i + 10); 9 | }; -------------------------------------------------------------------------------- /lib/metadata/getFragmentMetadata.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Generates the metadata for stale project fragment assets. 4 | */ 5 | 6 | const { sep } = require("path"); 7 | const config = require("../config"); 8 | const getPublicDestPath = require("./getPublicDestPath"); 9 | const getArticleDateFromPath = require("./getArticleDateFromPath"); 10 | const { 11 | mdToHtml, 12 | publicPathToUrl, 13 | getCachedMatter, 14 | toArray, 15 | warn 16 | } = require("../utils"); 17 | 18 | const blogMetadata = metadata => { 19 | if (metadata.path.startsWith(config.sourceArticles)) { 20 | metadata.articleDate = getArticleDateFromPath(metadata.path); 21 | metadata.matter.data.category = metadata.matter.data.category 22 | ? toArray(metadata.matter.data.category) 23 | : []; 24 | metadata.matter.data.tag = metadata.matter.data.tag 25 | ? toArray(metadata.matter.data.tag) 26 | : []; 27 | } 28 | return metadata; 29 | }; 30 | 31 | const commonMetadata = (metadata) => { 32 | metadata.matter.data.template = `${config.templates}${sep}${metadata.matter.data.template}`; 33 | metadata.matter.data.title = metadata.matter.data.title || "Document"; 34 | metadata.matter.data.appendToTarget = metadata.matter.data.appendToTarget || false; 35 | metadata.matter.content = mdToHtml(metadata.path, metadata.matter.content); 36 | metadata.matter.excerpt = mdToHtml(metadata.path, metadata.matter.excerpt); 37 | metadata.destPath = getPublicDestPath(metadata); 38 | metadata.url = encodeURI(publicPathToUrl(metadata.destPath)); 39 | return metadata; 40 | }; 41 | 42 | const isMatterMissingRequiredProperties = (matter, path) => { 43 | const warningMsg = "Warning: Discarding page fragment - missing"; 44 | let missing = false; 45 | 46 | // front matter validations for all fragment types including generators 47 | if (!matter.data.hasOwnProperty("template") || !matter.data.template) { 48 | warn(`${warningMsg} "template" property: ${path}`); 49 | missing = true; 50 | } 51 | // front matter validations for fragments that are generators 52 | if (matter.data.hasOwnProperty("collection") && !matter.data.collection || 53 | matter.data.hasOwnProperty("collection") && !matter.data.collection.filterFn) { 54 | warn(`${warningMsg} "collection filterFn" property: ${path}`); 55 | missing = true; 56 | } 57 | return missing; 58 | }; 59 | 60 | module.exports = resolvedFragmentStats => { 61 | const metadata = []; 62 | for (const resolvedFragmentStat of resolvedFragmentStats) { 63 | const m = getCachedMatter(resolvedFragmentStat.path); 64 | // validate fragments have required front matter properties 65 | if (!isMatterMissingRequiredProperties(m, resolvedFragmentStat.path)) { 66 | resolvedFragmentStat.matter = m; 67 | metadata.push(resolvedFragmentStat); 68 | } 69 | }; 70 | return metadata.map(metadata => commonMetadata(blogMetadata(metadata))); 71 | }; -------------------------------------------------------------------------------- /lib/metadata/getPublicDestPath.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Write the source file to the target folder reflecting its path structure 4 | * as well as any permalink which may target it. 5 | * 6 | * Blog article's are specific use cases - their paths are "composed" from 7 | * the categories they may or may not declare and their destructured file names: 8 | * (yyyy-mm-dd-filename.[html|md]) -> 9 | * [target folder]/[category path segment[, ...]]/yyyy/mm/dd/filename.html. 10 | * All links are generated as "pretty links", meaning all links to pages are 11 | * links to their containing folders and all pages are named index.html and are 12 | * served when their containing folders are navigated to. 13 | */ 14 | 15 | const { sep, parse, join } = require("path"); 16 | const config = require("../config"); 17 | const isDateValid = require("../utils/dateValidator"); 18 | const { log, normalizeFileName } = require("../utils"); 19 | 20 | const prefaceMMDDWithZero = str => str.length === 1 ? `0${str}` : str; 21 | 22 | const showBadDateMessage = articleFileName => { 23 | log(`Error: Incorrect blog article file name found for article "${articleFileName}"`); 24 | log("Error: Blog article file names must be of the form: \"yyyy-mm-dd-filename.[html|md]\""); 25 | log("Error: Blog article file names must resolve to a valid date"); 26 | }; 27 | 28 | const getArticleDestPath = articleFileName => { 29 | // validate article file name matches expected pattern 30 | const found = /(\d{4})-(\d{2})-(\d{2})/.test(articleFileName.substring(0, 10)); 31 | if (!found) { 32 | showBadDateMessage(articleFileName); 33 | process.exit(); 34 | } 35 | 36 | // validate article file name date 37 | const parts = articleFileName.substring(0, 10).split("-"); 38 | const yyyy = Number(parts[0]); 39 | const mm = Number(parts[1]); 40 | const dd = Number(parts[2]); 41 | if (isNaN(yyyy) || isNaN(mm) || isNaN(dd) || !isDateValid(mm, dd, yyyy)) { 42 | showBadDateMessage(articleFileName); 43 | process.exit(); 44 | } 45 | 46 | // return the article's destination path 47 | const fileName = articleFileName.substring(11); 48 | return join(yyyy.toString(), prefaceMMDDWithZero(mm.toString()), prefaceMMDDWithZero(dd.toString()), parse(fileName).name, "index.html"); 49 | }; 50 | 51 | const getArticleCategoryPath = category => 52 | category.length > 0 ? category.join(`${sep}`).toLowerCase() : category; 53 | 54 | const applyPermalink = (metadata, destinationPath) => { 55 | if (metadata.matter.data.permalink) { 56 | return join(config.targetFolder, metadata.matter.data.permalink); 57 | } 58 | if (config.userConfig.permalinks) { 59 | const match = config.userConfig.permalinks.find(permalink => 60 | destinationPath.startsWith(join(config.targetFolder, permalink.targetFolder))); 61 | // begining with v4.0.0 permalinks preserve the paths to child folders of targetFolder 62 | const path = match && join(destinationPath.replace(match.targetFolder, match.path)) || destinationPath; 63 | return path; 64 | } 65 | return destinationPath; 66 | }; 67 | 68 | module.exports = metadata => { 69 | let destinationPath; 70 | // determine public base path 71 | const baseDir = parse(metadata.path).dir.replace(config.fragments, config.targetFolder); 72 | const fileName = normalizeFileName(parse(metadata.path).base); 73 | if (parse(metadata.path).dir.startsWith(`${config.sourceArticles}`)) { 74 | // article pages 75 | // we don't want to include the articles folder in the path so this will resolve to targetFolder/ 76 | const categoryPath = getArticleCategoryPath(metadata.matter.data.category); 77 | const articlePath = getArticleDestPath(fileName); 78 | destinationPath = categoryPath.length > 0 79 | ? join(applyPermalink(metadata, baseDir), categoryPath, articlePath) 80 | : join(applyPermalink(metadata, baseDir), articlePath); 81 | } else { 82 | // all other pages 83 | destinationPath = parse(fileName).name === "index" && 84 | join(applyPermalink(metadata, baseDir), "index.html") || 85 | join(applyPermalink(metadata, join(baseDir, parse(fileName).name)), "index.html"); 86 | } 87 | return destinationPath.toLowerCase(); 88 | }; -------------------------------------------------------------------------------- /lib/metadata/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | createSortedTagCatalog: require("./createSortedTagCatalog"), 4 | createSortedArticlesCatalog: require("./createSortedArticlesCatalog"), 5 | createSortedCategoriesCatalog: require("./createSortedCategoriesCatalog"), 6 | getFragmentMetadata: require("./getFragmentMetadata"), 7 | getAllIncludesMetadata: require("./getAllIncludesMetadata"), 8 | createRelatedArticlesByTag: require("./createRelatedArticlesByTag"), 9 | createRelatedArticlesByTagFlattened: require("./createRelatedArticlesByTagFlattened"), 10 | createRelatedArticlesByCategory: require("./createRelatedArticlesByCategory"), 11 | getArticleDateFromPath: require("./getArticleDateFromPath"), 12 | getAllData: require("./getAllData"), 13 | getPublicDestPath: require("./getPublicDestPath") 14 | }; -------------------------------------------------------------------------------- /lib/metrics/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Uses process.hrtime(time) for nano second precision. 4 | * 5 | * api: startTimer, stopTimer, getTimer, forEachTimer, clearTimers 6 | */ 7 | 8 | const timers = new Map(); 9 | 10 | const startTimer = (name, precision = 3) => { 11 | const timer = { 12 | name, 13 | precision, 14 | started: process.hrtime() 15 | }; 16 | timers.set(name, timer); 17 | }; 18 | 19 | const stopTimer = name => { 20 | const timer = timers.get(name); 21 | if (timer) { 22 | // divide by a million to get nano to mills 23 | const mills = process.hrtime(timer.started)[1] / 1000000; 24 | // print message + time 25 | timer.elapsed = 26 | timer.name + 27 | ": " + 28 | process.hrtime(timer.started)[0] + 29 | " s, " + 30 | mills.toFixed(timer.precision) + 31 | " ms"; 32 | return timer.elapsed; 33 | } 34 | }; 35 | 36 | const getTimer = name => timers.has(name) && timers.get(name); 37 | 38 | const forEach = (callbackFn, thisArg) => timers.size > 0 && timers.forEach(callbackFn, thisArg); 39 | 40 | const deleteTimer = name => timers.has(name) && timers.delete(name); 41 | 42 | const clearTimers = () => timers.clear(); 43 | 44 | module.exports = { 45 | startTimer, 46 | stopTimer, 47 | getTimer, 48 | forEach, 49 | deleteTimer, 50 | clearTimers 51 | }; 52 | -------------------------------------------------------------------------------- /lib/stats/getStats.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Obtains the stats (a snapshot) of one or more files. 4 | * Returns an array of objects with properties path, modified date, 5 | * stale indicator and type. Can be used to get a list of stale 6 | * files when compared against a time stamp. 7 | */ 8 | 9 | const { parse } = require("path"); 10 | const config = require("../config"); 11 | const { statsFileName } = require("../config/fileNames"); 12 | const { 13 | getCachedMatter, 14 | doesCachedStatsFileExists, 15 | getFileModifiedTime 16 | } = require("../utils"); 17 | 18 | module.exports = assets => { 19 | const assignType = path => { 20 | const dir = parse(path).dir; 21 | return dir === config.callbacks && "callback" || 22 | dir.startsWith(config.callbacks) && dir !== config.callbacks && !dir.startsWith(config.filters) && "module" || 23 | dir === config.filters && "filter" || 24 | dir.startsWith(config.fragments) && getCachedMatter(path).data.collection && "generator" || 25 | dir.startsWith(config.fragments) && "fragment" || 26 | dir === config.includes && "include" || 27 | dir === config.templates && "template" || 28 | dir === config.sourceData && "data" || 29 | "*"; 30 | }; 31 | 32 | const isAlwaysBuild = path => { 33 | if (path.startsWith(config.fragments)) { 34 | const matter = getCachedMatter(path); 35 | if (matter.data.alwaysBuild && matter.data.alwaysBuild === true) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | }; 41 | 42 | const isGenerator = path => { 43 | if (path.startsWith(config.fragments)) { 44 | const matter = getCachedMatter(path); 45 | if (matter.data.collection) { 46 | return true; 47 | } 48 | } 49 | return false; 50 | }; 51 | 52 | const timestampMs = doesCachedStatsFileExists() && getFileModifiedTime(statsFileName) || 0; 53 | 54 | const makeStat = path => { 55 | const mtimeMs = getFileModifiedTime(path); 56 | const isStale = mtimeMs > timestampMs || isAlwaysBuild(path) || isGenerator(path); 57 | return { 58 | path, 59 | mtimeMs, 60 | type: assignType(path), 61 | isStale 62 | }; 63 | }; 64 | 65 | return assets.map(makeStat); 66 | }; 67 | -------------------------------------------------------------------------------- /lib/tasks/cache-bust/cacheBust.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { existsSync, ensureFile } = require("fs-extra"); 4 | const buster = require("@4awpawz/buster"); 5 | const { sep } = require("path"); 6 | const metrics = require("../../metrics"); 7 | const { log } = require("../../utils"); 8 | const fileNames = require("../../config/fileNames"); 9 | 10 | const writeCacheBustLockFile = async () => await ensureFile(fileNames.cacheBustedLockFileName); 11 | 12 | const isAlreadyCacheBusted = () => existsSync(fileNames.cacheBustedLockFileName); 13 | 14 | module.exports = async (options) => { 15 | metrics.clearTimers(); 16 | metrics.startTimer("cache busting"); 17 | if (isAlreadyCacheBusted()) { 18 | throw new Error("release build has already been cache busted."); 19 | } 20 | const releaseFolder = "release"; 21 | const opts = {}; 22 | opts.verbose = options.includes("-V") || options.includes("-v"); 23 | opts.manifest = options.includes("-V") || options.includes("-m"); 24 | await buster({ 25 | options: opts, 26 | directives: [ 27 | `${releaseFolder}${sep}media${sep}**${sep}*.*:1`, 28 | `${releaseFolder}${sep}css${sep}**${sep}*.map:1`, 29 | `${releaseFolder}${sep}scripts${sep}**${sep}*.map:1`, 30 | `${releaseFolder}${sep}**${sep}*.html:2`, 31 | `${releaseFolder}${sep}css${sep}**${sep}*.css:3`, 32 | `${releaseFolder}${sep}scripts${sep}**${sep}*.js:3` 33 | ] 34 | }); 35 | await writeCacheBustLockFile(); 36 | metrics.stopTimer("cache busting"); 37 | metrics.forEach(timer => log(timer.elapsed)); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/tasks/create-new-project/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fs = require("fs-extra"); 3 | const { join, resolve } = require("path"); 4 | const log = require("../../utils/log"); 5 | const globFriendly = require("../../utils/globFriendly"); 6 | 7 | module.exports = (target, options) => { 8 | try { 9 | log("*** trio-new ***"); 10 | 11 | const msg = "*** Please enter a path and try again."; 12 | 13 | if (!target) { 14 | log("*** Error: Missing path parameter"); 15 | log(msg); 16 | process.exit(); 17 | } 18 | 19 | if (fs.existsSync(target) && globFriendly(join(target, "*.*")).length > 0) { 20 | log(`*** Error: The target folder "${resolve(target)}" is not empty.`); 21 | log(msg); 22 | process.exit(); 23 | } 24 | 25 | log(`*** The target folder is "${resolve(target)}"`); 26 | 27 | if (options.length === 0) { 28 | // create empty project structure 29 | log("*** Creating new project. Please wait..."); 30 | fs.ensureDirSync(join(target, "source", "callbacks")); 31 | fs.ensureDirSync(join(target, "source", "filters")); 32 | fs.ensureDirSync(join(target, "source", "css")); 33 | fs.ensureDirSync(join(target, "source", "etc")); 34 | fs.ensureDirSync(join(target, "source", "data")); 35 | fs.ensureDirSync(join(target, "source", "fragments", "articles")); 36 | fs.ensureDirSync(join(target, "source", "includes")); 37 | fs.ensureDirSync(join(target, "source", "lib")); 38 | fs.ensureDirSync(join(target, "source", "media")); 39 | fs.ensureDirSync(join(target, "source", "sass")); 40 | fs.ensureDirSync(join(target, "source", "scripts")); 41 | fs.ensureDirSync(join(target, "source", "templates")); 42 | const gitIgnore = "node_modules\npublic\nrelease\ntrio.manifest.json\n.cache"; 43 | fs.writeFileSync(join(target, ".gitignore"), gitIgnore); 44 | fs.writeJsonSync(join(target, "trio.json"), {}); 45 | } else { 46 | const savedCWD = process.cwd(); 47 | process.chdir(__dirname); 48 | const from = join("..", "..", "..", "scaffolding", "reference-project-1"); 49 | const to = join(savedCWD, target); 50 | fs.copySync(from, to); 51 | // IMPORTANT: because git doesn't track empty folders, 52 | // the following folders need to be added to source 53 | fs.ensureDirSync(join(to, "source", "css")); 54 | fs.ensureDirSync(join(to, "source", "etc")); 55 | fs.ensureDirSync(join(to, "source", "lib")); 56 | fs.ensureDirSync(join(to, "source", "media")); 57 | fs.ensureDirSync(join(to, "source", "scripts")); 58 | process.chdir(savedCWD); 59 | } 60 | 61 | log(`*** ${resolve(target)} created`); 62 | } catch (error) { 63 | log("*** An error has occurred and processing has terminated."); 64 | log(error); 65 | process.exit(); 66 | } 67 | }; -------------------------------------------------------------------------------- /lib/tasks/file-watcher/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Note: Chokidar doesn't have a "rename" event. When a file is renamed, it fires an unlink event, 4 | * followed by an add event. This behaviour causes Trio to throw numerous exceptions and mangle the 5 | * generation of meta data for the fragment reported as having been added when building incrementally. 6 | * Therefore, in response to a "delete" event, the stats file is deleted along with the public folder, 7 | * which will trigger Trio to do a one-off build as if "trio b" was run from the command line. 8 | */ 9 | 10 | const chokidar = require("chokidar"); 11 | const { parse, sep } = require("path"); 12 | const config = require("../../config"); 13 | const build = require("../../../index"); 14 | const { getIgnoredSourceFolders, log, triggerOneOffBuild } = require("../../utils"); 15 | 16 | const isFileChildOfIgnored = (ignoredFolders, file) => { 17 | for (const ignoredFolder of ignoredFolders) { 18 | const filePath = parse(file).dir; 19 | if (filePath.indexOf(parse(ignoredFolder).dir) !== -1) { 20 | return true; 21 | } 22 | } 23 | return false; 24 | }; 25 | 26 | module.exports = () => { 27 | log("Starting chokidar"); 28 | 29 | // get ignored folders 30 | const ignored = getIgnoredSourceFolders(sep); 31 | 32 | // add files to be watched 33 | const watcher = chokidar.watch(`${config.source}${sep}**`, { 34 | ignoreInitial: true 35 | }); 36 | 37 | watcher.add("trio.json"); 38 | 39 | // add event listeners 40 | watcher 41 | .on("ready", () => { 42 | log(": Now watching source folder and trio.json"); 43 | }) 44 | .on("add", path => { 45 | log(" "); 46 | log(`: File ${path} has been added`); 47 | !isFileChildOfIgnored(ignored, path) && setTimeout(async () => { 48 | process.env.TRIO_ENV_buildIncrementally === "incremental-build" && triggerOneOffBuild(); 49 | await build(); 50 | }, 1); 51 | }) 52 | .on("change", path => { 53 | log(" "); 54 | log(`: File ${path} has been changed`); 55 | !isFileChildOfIgnored(ignored, path) && setTimeout(async () => { 56 | process.env.TRIO_ENV_serveInBrowser === "serve-in-browser" && 57 | path === "trio.json" && triggerOneOffBuild(); 58 | await build(); 59 | }, 1); 60 | }) 61 | .on("unlink", path => { 62 | log(" "); 63 | log(`: File ${path} has been removed`); 64 | !isFileChildOfIgnored(ignored, path) && setTimeout(async () => { 65 | process.env.TRIO_ENV_buildIncrementally === "incremental-build" && triggerOneOffBuild(); 66 | await build(); 67 | }, 1); 68 | }); 69 | }; -------------------------------------------------------------------------------- /lib/utils/allArayElsToLowerCase.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = ar => ar.map(e => e.toLowerCase()); -------------------------------------------------------------------------------- /lib/utils/articlesHaveCategories.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const config = require("../config"); 3 | 4 | module.exports = metadata => metadata.filter(item => item.path.startsWith(config.sourceArticles)) 5 | .some(item => item.matter.data.category && item.matter.data.category.length); -------------------------------------------------------------------------------- /lib/utils/articlesHaveTags.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const config = require("../config"); 3 | 4 | module.exports = metadata => metadata.filter(item => item.path.startsWith(config.sourceArticles)) 5 | .some(item => item.matter.data.tag && item.matter.data.tag.length); -------------------------------------------------------------------------------- /lib/utils/browserSync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { sep } = require("path"); 3 | const bsf = require("browser-sync"); 4 | const { findAPortNotInUse } = require("portscanner"); 5 | const { targetFolder } = require("../config"); 6 | 7 | module.exports = async () => { 8 | const bs = bsf.create(); 9 | const baseUrl = process.env.TRIO_ENV_baseUrl; 10 | 11 | bs.watch(`.${sep}${targetFolder}${sep}**${sep}*.html`, (event, file) => { 12 | bs.reload(file); 13 | }); 14 | bs.watch(`.${sep}${targetFolder}${sep}scripts${sep}**${sep}*.js`, (event, file) => { 15 | bs.reload(file); 16 | }); 17 | bs.watch(`.${sep}${targetFolder}${sep}css${sep}**${sep}*.css`, (event, file) => { 18 | bs.reload(file); 19 | }); 20 | bs.watch(`.${sep}${targetFolder}${sep}media${sep}**${sep}*.*`, (event, file) => { 21 | bs.reload(file); 22 | }); 23 | 24 | bs.init({ 25 | server: { 26 | baseDir: `.${sep}${targetFolder}`, 27 | middleware: (req, res, next) => { 28 | if (baseUrl && baseUrl.length > 0) { 29 | const url = req.url.slice(baseUrl.length); 30 | req.url = url.length === 0 ? sep : url; 31 | } 32 | return next(); 33 | } 34 | }, 35 | notify: false, 36 | port: await findAPortNotInUse(3000) 37 | }); 38 | }; -------------------------------------------------------------------------------- /lib/utils/callTagCallbacks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const cheerio = require("cheerio"); 3 | const importFresh = require("import-fresh"); 4 | const { sep } = require("path"); 5 | const config = require("../config"); 6 | const log = require("./log"); 7 | 8 | module.exports = async ($tag, callback, $, asset, siteMetadata) => { 9 | if (callback === null) { 10 | log(`*** An invalid callback declaration in ${asset.path} has been found and processing has terminated.`); 11 | log("*** Correct the issue and rerun your build."); 12 | process.exit(); 13 | } 14 | const ctx = { $tag, $page: $, asset, site: siteMetadata, cheerio }; 15 | try { 16 | await importFresh(`${process.cwd()}${sep}${config.callbacks}${sep}${callback}`)(ctx); 17 | } catch (error) { 18 | log(`Error: Exception thrown when calling tag-based callback ${callback}`); 19 | log(error); 20 | } 21 | }; -------------------------------------------------------------------------------- /lib/utils/createRelatedItem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // "" -> {} 3 | const relItemFromString = (articleDate, url, title, excerpt) => ({ 4 | articleDate, 5 | url, 6 | title, 7 | excerpt 8 | }); 9 | 10 | // {} -> {} 11 | const relItemFromObj = obj => ({ 12 | articleDate: obj.articleDate, 13 | url: obj.url, 14 | title: obj.matter.data.title, 15 | excerpt: obj.matter.excerpt 16 | }); 17 | 18 | module.exports = arg => 19 | typeof arg === "string" 20 | ? relItemFromString(...arg.split("\n")) 21 | : relItemFromObj(arg); -------------------------------------------------------------------------------- /lib/utils/dateValidator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const isYearValid = (y) => { 3 | return typeof (y) === "number"; 4 | }; 5 | 6 | const isMonthValid = (m) => { 7 | if (typeof (m) !== "number") { 8 | return false; 9 | } 10 | return m >= 1 && m <= 12; 11 | }; 12 | 13 | const isLeapYear = y => { 14 | // If year is not evenly divisible by 4 it isn"t a leap year 15 | if (y % 4) { 16 | return false; 17 | } 18 | // If year is evenly divisible by 4 and is not 19 | // evenly divisible by 100 it is a leap year 20 | if (y % 100) { 21 | return true; 22 | } 23 | // If a year is evenly divisible by 4 and it is evenly divisible 24 | // by 100 and it is evenly divisible by 400 it is a leap year 25 | return (y % 400 === 0); 26 | }; 27 | 28 | const isDayValid = (y, m, d) => { 29 | let daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 30 | // Validate numerics and min day range 31 | if (typeof (d) !== "number" || d < 1) { 32 | return false; 33 | } 34 | // Validate for leap year (2/29) 35 | if (m === 2 && d === 29) { 36 | return isLeapYear(y); 37 | } 38 | // Validate for all other dates 39 | if (d <= daysInMonths[m - 1]) { 40 | return true; 41 | } 42 | return false; 43 | }; 44 | 45 | module.exports = (m, d, y) => { 46 | return (isYearValid(y) && isMonthValid(m) && isDayValid(y, m, d)); 47 | }; -------------------------------------------------------------------------------- /lib/utils/doesCachedStatsFileExists.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { existsSync } = require("fs-extra"); 3 | const { statsFileName } = require("../config/fileNames"); 4 | 5 | module.exports = () => existsSync(statsFileName); -------------------------------------------------------------------------------- /lib/utils/getAllIncludes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = $ => 3 | $("[data-trio-include]").toArray() 4 | .map(element => $(element).data("trio-include")); 5 | -------------------------------------------------------------------------------- /lib/utils/getCachedStatsFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Inforeces the fact that the cached stats file is always read only. 4 | */ 5 | 6 | const importFresh = require("import-fresh"); 7 | const { statsFileName } = require("../config/fileNames"); 8 | 9 | module.exports = () => importFresh(statsFileName); -------------------------------------------------------------------------------- /lib/utils/getFileModifiedTime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Returns the time in ms when file's (path) data was last modified 4 | */ 5 | 6 | const { existsSync, statSync } = require("fs-extra"); 7 | const log = require("./log"); 8 | 9 | const error = message => { 10 | log(message); // needed to avoid circular reference! 11 | process.exit(); 12 | }; 13 | 14 | module.exports = path => { 15 | existsSync(path) || error(`Error: File ${path} doesn't exist`); 16 | return statSync(path).mtimeMs; 17 | }; -------------------------------------------------------------------------------- /lib/utils/getIgnoredSourceFolders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const config = require("../config"); 3 | 4 | module.exports = sep => config.userConfig.ignore.map(path => `source${sep}${path}/**`); 5 | -------------------------------------------------------------------------------- /lib/utils/getTagCallbackDependencies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Returns an array of callback dependencies. 4 | */ 5 | 6 | module.exports = $composite => { 7 | const callbacks = []; 8 | $composite("[data-trio-callback]") 9 | .toArray() 10 | .forEach(tag => { 11 | const $tag = $composite(tag); 12 | callbacks.push($tag.data("trio-callback").trim()); 13 | }); 14 | return callbacks; 15 | }; -------------------------------------------------------------------------------- /lib/utils/getTagCallbacks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Returns an array of items which contain: 4 | * 1) a cheerio object that wraps the tag containing the callback 5 | * 2) the name of the callback to be called 6 | */ 7 | 8 | module.exports = $composite => { 9 | return $composite("[data-trio-callback]") 10 | .toArray() 11 | .map(tag => { 12 | const $tag = $composite(tag); 13 | return {$tag, callback: $tag.data("trio-callback")}; 14 | }); 15 | }; -------------------------------------------------------------------------------- /lib/utils/getTemplateFileName.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * templateFileName -> templateFileName + ".html" 4 | */ 5 | 6 | const { parse } = require("path"); 7 | 8 | module.exports = templateFileName => 9 | parse(templateFileName).ext === "" && templateFileName + ".html" || templateFileName; -------------------------------------------------------------------------------- /lib/utils/globFriendly.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * A friendlier glob because it converts back slashes 4 | * in glob patterns to glob's required forward slashes. 5 | */ 6 | 7 | const glob = require("glob"); 8 | const { sep } = require("path"); 9 | 10 | module.exports = (pattern, options) => glob.sync(pattern.split(sep).join("/"), options); -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | dateValidator: require("./dateValidator"), 4 | metadataHasPath: require("./metadataHasPath"), 5 | articlesHaveTags: require("./articlesHaveTags"), 6 | articlesHaveCategories: require("./articlesHaveCategories"), 7 | metadataHasArticles: require("./metadataHasArticles"), 8 | normalizeFileName: require("./normalizeFileName"), 9 | sortArticles: require("./sortArticles"), 10 | publicPathToUrl: require("./publicPathToUrl"), 11 | createRelatedItem: require("./createRelatedItem"), 12 | callTagCallbacks: require("./callTagCallbacks"), 13 | toArray: require("./toArray"), 14 | browserSync: require("./browserSync"), 15 | getAllIncludes: require("./getAllIncludes"), 16 | readCache: require("./readCache").readCache, 17 | clearReadCache: require("./readCache").clearCache, 18 | deleteCachedFile: require("./readCache").deleteCachedFile, 19 | deleteCachedMatter: require("./matterCache").deleteCachedMatter, 20 | getCachedMatter: require("./matterCache").getCachedMatter, 21 | clearMatterCache: require("./matterCache").clearCache, 22 | mdToHtml: require("./mdToHtml"), 23 | getFileModifiedTime: require("./getFileModifiedTime"), 24 | doesCachedStatsFileExists: require("./doesCachedStatsFileExists"), 25 | writeCachedStatsFile: require("./writeCachedStatsFile"), 26 | writeCachedMetaFile: require("./writeCachedMetaFile"), 27 | log: require("./log"), 28 | getCachedStatsFile: require("./getCachedStatsFile"), 29 | globFriendly: require("./globFriendly"), 30 | getTagCallbacks: require("./getTagCallbacks"), 31 | getTagCallbackDependencies: require("./getTagCallbackDependencies"), 32 | isFile: require("./isFile"), 33 | resolveIndirectPathToInclude: require("./resolveIndirectPathToInclude"), 34 | getTemplateFileName: require("./getTemplateFileName"), 35 | showDeprecated: require("./showDeprecated"), 36 | warn: require("./warn"), 37 | isPlural: require("./isPlural"), 38 | triggerOneOffBuild: require("./triggerOneOffBuild"), 39 | getIgnoredSourceFolders: require("./getIgnoredSourceFolders") 40 | }; -------------------------------------------------------------------------------- /lib/utils/isFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { statSync } = require("fs-extra"); 3 | 4 | module.exports = path => statSync(path).isFile(); -------------------------------------------------------------------------------- /lib/utils/isPlural.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Determines if a number is plural. 4 | * All numbers greater than or equal to 0 except 1 are considered plural. 5 | */ 6 | 7 | module.exports = n => n >= 0 && n !== 1; 8 | -------------------------------------------------------------------------------- /lib/utils/log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = console.log.bind(console); -------------------------------------------------------------------------------- /lib/utils/matterCache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Get front matter from cache. 4 | */ 5 | 6 | const matter = require("gray-matter"); 7 | const { frontMatterDelimiters, frontMatterSeparator } = require("../config"); 8 | const { readCache, deleteCachedFile } = require("./readCache"); 9 | 10 | const fmMap = new Map(); 11 | 12 | exports.clearCache = () => fmMap.clear(); 13 | 14 | exports.deleteCachedMatter = path => { 15 | fmMap.delete(path); 16 | deleteCachedFile(path); 17 | }; 18 | 19 | exports.getCachedMatter = (path, options = { 20 | delimiters: frontMatterDelimiters, 21 | excerpt: true, 22 | excerpt_separator: frontMatterSeparator 23 | }) => { 24 | !fmMap.has(path) && fmMap.set(path, matter(readCache(path), options)); 25 | return fmMap.get(path); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/utils/mdToHtml.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Converts content to html if it is markdown. 4 | */ 5 | 6 | const marked = require("marked"); 7 | const { parse } = require("path"); 8 | 9 | module.exports = (path, content) => 10 | parse(path).ext === ".html" ? content : marked(content); -------------------------------------------------------------------------------- /lib/utils/metadataHasArticles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const config = require("../config"); 3 | 4 | module.exports = frags => 5 | frags.some(frag => frag.path.startsWith(config.sourceArticles)); -------------------------------------------------------------------------------- /lib/utils/metadataHasPath.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = metadata => (key, path) => 3 | metadata.some(item => item[key].startsWith(path)); -------------------------------------------------------------------------------- /lib/utils/normalizeFileName.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = fileName => fileName.startsWith("_") 3 | ? fileName.substring(1) 4 | : fileName; -------------------------------------------------------------------------------- /lib/utils/publicPathToUrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { sep } = require("path"); 3 | 4 | module.exports = path => { 5 | const t = path.substring(path.indexOf(`${sep}`)); 6 | const tt = t.indexOf("index.html") !== -1 7 | ? t.substring(0, t.indexOf("index.html")) 8 | : t; 9 | const regex = /\\/g; 10 | return tt.replace(regex, "/"); 11 | }; -------------------------------------------------------------------------------- /lib/utils/readCache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * File cache management 4 | */ 5 | 6 | const { parse } = require("path"); 7 | const { existsSync, readFileSync, readJSONSync } = require("fs-extra"); 8 | 9 | const rCache = new Map(); 10 | 11 | const error = message => { 12 | throw new Error(message); 13 | }; 14 | 15 | const fileExists = path => 16 | existsSync(path) || error(`Error: File ${path} doesn't exist`); 17 | 18 | const cacheJSONFile = (path, options) => 19 | rCache.set(path, readJSONSync(path, options)); 20 | 21 | const cacheFile = (path, options) => 22 | rCache.set(path, readFileSync(path, options)); 23 | 24 | exports.deleteCachedFile = (path) => 25 | rCache.delete(path); 26 | 27 | exports.readCache = (path, options = "utf8") => { 28 | !rCache.has(path) && fileExists(path) && 29 | (parse(path).ext === ".json" && cacheJSONFile(path, options) || 30 | cacheFile(path, options)); 31 | return rCache.get(path); 32 | }; 33 | 34 | exports.clearCache = () => rCache.clear(); -------------------------------------------------------------------------------- /lib/utils/resolveIndirectPathToInclude.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { parse } = require("path"); 3 | const getCachedMatter = require("./matterCache").getCachedMatter; 4 | 5 | module.exports = (templatePath, dataKey, pathsToFragments) => { 6 | // find the fragment associated with the template 7 | const fragmentPath = pathsToFragments.find(fragmentPath => 8 | getCachedMatter(fragmentPath).data.template === parse(templatePath).base); 9 | // if found then return the property value for dataKey - will 10 | // return undefined if fragment wasn't found (not implemented yet) or 11 | // if found fragment's matter.data doesn't have a key by that name 12 | if (fragmentPath) { 13 | const fragmentMatter = getCachedMatter(fragmentPath); 14 | return fragmentMatter.data[dataKey]; 15 | } 16 | }; -------------------------------------------------------------------------------- /lib/utils/showDeprecated.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { warn } = require("./warn"); 3 | 4 | module.exports = (when, what, nextVersion, message) => 5 | warn(`Warning: As of ${when} "${what}" has been deprecated and will be removed in ${nextVersion}. ${message}`); 6 | -------------------------------------------------------------------------------- /lib/utils/sortArticles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (a, b) => { 3 | const aa = a.split("\n"); 4 | const bb = b.split("\n"); 5 | const result = parseInt(bb[0].split("-").join(""), 10) - 6 | parseInt(aa[0].split("-").join(""), 10); 7 | // compare on date and then title if dates are equal 8 | return result !== 0 9 | ? result : a[2] === b[2] 10 | ? 0 : a[2] < b[2] 11 | ? -1 : 1; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/toArray.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * arg -> [arg] 4 | */ 5 | 6 | module.exports = arg => typeof arg === "undefined" && [] || Array.isArray(arg) && arg || [arg]; -------------------------------------------------------------------------------- /lib/utils/triggerOneOffBuild.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Triggers a one-off build by deleting the stats file and the public folder. 4 | */ 5 | 6 | const { removeSync } = require("fs-extra"); 7 | const statsFileName = require("../config/fileNames").statsFileName; 8 | const publicFolderName = require("../config").targetFolder; 9 | 10 | module.exports = () => { 11 | removeSync(statsFileName); 12 | removeSync(publicFolderName); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/warn.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = console.warn.bind(console); 3 | -------------------------------------------------------------------------------- /lib/utils/writeCachedMetaFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { ensureDirSync, writeJSONSync } = require("fs-extra"); 3 | const { parse } = require("path"); 4 | const { metaFileName } = require("../config/fileNames"); 5 | 6 | module.exports = meta => { 7 | ensureDirSync(parse(metaFileName).dir); 8 | writeJSONSync(metaFileName, meta, { spaces: "" }); 9 | }; -------------------------------------------------------------------------------- /lib/utils/writeCachedStatsFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { ensureDirSync, writeJSONSync } = require("fs-extra"); 3 | const { parse } = require("path"); 4 | const { statsFileName } = require("../config/fileNames"); 5 | 6 | module.exports = stats => { 7 | ensureDirSync(parse(statsFileName).dir); 8 | writeJSONSync(statsFileName, stats, { spaces: "" }); 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4awpawz/trio", 3 | "version": "6.1.0", 4 | "description": "Fast, simple yet powerful JavaScript-driven static site generation.", 5 | "keywords": [ 6 | "static-site-generator", 7 | "static-website-generator", 8 | "site-generator", 9 | "website-generator", 10 | "blog-engine", 11 | "blog-aware", 12 | "markup", 13 | "markdown", 14 | "html", 15 | "javascript", 16 | "cheerio", 17 | "browsersync" 18 | ], 19 | "homepage": "https://gettriossg.com", 20 | "main": "index.js", 21 | "bin": { 22 | "trio": "bin/trio.js" 23 | }, 24 | "scripts": { 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "author": "Jeffrey Schwartz", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/4awpawz/trio" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/4awpawz/trio/issues", 34 | "email": "4awpawz@gmail.com" 35 | }, 36 | "license": "MIT", 37 | "devDependencies": { 38 | "eslint": "^7.25.0", 39 | "eslint-config-standard": "^16.0.2", 40 | "eslint-plugin-import": "^2.22.1", 41 | "eslint-plugin-node": "^11.1.0", 42 | "eslint-plugin-promise": "^5.1.0", 43 | "eslint-plugin-standard": "^4.1.0" 44 | }, 45 | "dependencies": { 46 | "@4awpawz/buster": "^1.0.0", 47 | "autoprefixer": "^10.2.5", 48 | "browser-sync": "^2.26.14", 49 | "caniuse-lite": "^1.0.30001223", 50 | "cheerio": "^0.22.0", 51 | "chokidar": "^3.5.1", 52 | "cssnano": "^5.0.2", 53 | "cssnano-preset-default": "^5.0.1", 54 | "fs-extra": "^10.0.0", 55 | "glob": "^7.1.7", 56 | "gray-matter": "^4.0.3", 57 | "import-fresh": "^3.3.0", 58 | "js-beautify": "^1.13.13", 59 | "marked": "^2.0.3", 60 | "portscanner": "^2.2.0", 61 | "postcss": "^8.2.14", 62 | "sass": "^1.32.12" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | release 4 | trio.manifest.json 5 | .cache -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/archivePage.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, site }) => { 2 | site.articlesCatalog.forEach(article => { 3 | $tag.append(/* html */ ` 4 |
  • 5 | 7 | ${article.matter.data.articleTitle} posted to ${article.matter.data.category[0]} on ${article.articleDate} 8 | 9 |
  • 10 | `); 11 | }); 12 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/articleNavigator.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset }) => { 2 | if (asset.previousArticleUrl.length > 0) { 3 | $tag.append(/* html */` 4 | Older 5 | `); 6 | } else { 7 | $tag.append(/* html */` 8 | Older 9 | `); 10 | } 11 | if (asset.nextArticleUrl.length > 0) { 12 | $tag.append(/* html */` 13 | Newer 14 | `); 15 | } else { 16 | $tag.append(/* html */` 17 | Newer 18 | `); 19 | } 20 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/articlePage.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset }) => { 2 | $tag.find("h1.article__title").append(asset.matter.data.articleTitle); 3 | $tag.find("div.article__date").append(`Posted to ${asset.matter.data.category[0]} on ${asset.articleDate}`); 4 | $tag.find("section.article__content").append(asset.matter.content); 5 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/blogNavigator.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset, site }) => { 2 | const totalPages = asset.collection.totalItems; 3 | const index = asset.collection.index; 4 | const blogFolderName = site.userConfig.blogFolderName; 5 | 6 | // older 7 | if (index === totalPages - 1) { 8 | $tag.append(/* html */` 9 | Older 10 | `); 11 | } else { 12 | $tag.append(/* html */` 13 | Older 14 | `); 15 | } 16 | // newer 17 | if (index === 0) { 18 | $tag.append(/* html */` 19 | Newer 20 | `); 21 | } else { 22 | $tag.append(/* html */` 23 | Newer 24 | `); 25 | } 26 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/blogPage.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset }) => { 2 | asset.collection.data.forEach(article => $tag.append(/* html */` 3 |
  • 4 |
    5 |
    6 |

    Image

    7 |
    8 |

    9 | ${article.matter.data.articleTitle} 10 |

    11 | 12 |

    ${article.matter.excerpt}

    13 |
    14 |
  • 15 | `)); 16 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/categoriesList.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, site }) => { 2 | site.categoriesCatalog.forEach(item => { 3 | $tag.append(/* html */` 4 |
  • 5 | ${item.category} 6 |
  • 7 | `); 8 | }); 9 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/categoryPage.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset, site }) => { 2 | asset.collection.data.related.forEach(relatedArticle => { 3 | const article = site.frags.find(frag => frag.url === relatedArticle.url); 4 | $tag.append(/* html */` 5 |
  • 6 | 7 | ${article.matter.data.articleTitle} posted on ${relatedArticle.articleDate} 8 | 9 |
  • 10 | `); 11 | }); 12 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/footer.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, site }) => { 2 | $tag.append(site.timestamp); 3 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/getBrand.js: -------------------------------------------------------------------------------- 1 | /* 2 | dataDependencies: brand 3 | */ 4 | module.exports = ({ $tag, site }) => $tag 5 | .append(site.dataCatalog.brand.brandName); -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/header.js: -------------------------------------------------------------------------------- 1 | /* 2 | dataDependencies: brand 3 | */ 4 | module.exports = ({ $tag, site, asset }) => { 5 | $tag.find(".header__brand").append(site.dataCatalog.brand.brandName); 6 | const activeHeaderItem = asset.matter.data.activeHeaderItem; 7 | $tag.find(`li.header__nav-item:nth-child(${activeHeaderItem})`) 8 | .find("a.header__nav-item-link") 9 | .addClass("header__nav-item-link--current"); 10 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/relatedArticlesList.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset, site }) => { 2 | site.articlesCatalog 3 | .filter(article => article.matter.data.category[0] === asset.matter.data.category[0] && 4 | article.url !== asset.url) 5 | .forEach(item => { 6 | $tag.append(/* html */ ` 7 |
  • 8 | ${item.matter.data.articleTitle} : posted to ${item.matter.data.category[0]} on ${item.articleDate} 10 |
  • 11 | `); 12 | }); 13 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/setBlogPageDocTitle.js: -------------------------------------------------------------------------------- 1 | /* 2 | dataDependencies: 3 | - brand 4 | */ 5 | module.exports = ({ $tag, asset, site }) => { 6 | $tag.prepend(`${site.dataCatalog.brand.brandName} `); 7 | $tag.append(` Page ${asset.collection.index + 1} of ${asset.collection.totalItems}`); 8 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/setCategoryPageDocTitle.js: -------------------------------------------------------------------------------- 1 | /* 2 | dataDependencies: brand 3 | */ 4 | module.exports = ({ $tag, asset, site }) => { 5 | $tag.prepend(`${site.dataCatalog.brand.brandName} `); 6 | $tag.append(` ${asset.collection.data.category}`); 7 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/setCategoryPageTitle.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ $tag, asset }) => { 2 | $tag.prepend(`${asset.collection.data.category} `); 3 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/callbacks/setDocTitle.js: -------------------------------------------------------------------------------- 1 | /* 2 | dataDependencies: brand 3 | */ 4 | module.exports = ({ $tag, site }) => { 5 | $tag.prepend(`${site.dataCatalog.brand.brandName} `); 6 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/data/brand.json: -------------------------------------------------------------------------------- 1 | { 2 | "brandName": "Brand" 3 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/data/social.json: -------------------------------------------------------------------------------- 1 | { 2 | "twitter": "@Brand" 3 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/filters/archivePageArticlesFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return all the articles in the articlesCatalog. 3 | */ 4 | module.exports = ({ site }) => [{ 5 | pageName: "index", 6 | data: site.articlesCatalog 7 | }]; 8 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/filters/blogPagesArticlesFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a subset of articles for each blog page 3 | * and name each page, except for the 1st page, 4 | * which should be named index.html, using "page" 5 | * + the iterator's index value. 6 | */ 7 | module.exports = ({ collection, site }) => { 8 | const totalPages = 9 | Math.ceil(site.articlesCatalog.length / collection.articlesPerPage); 10 | const pages = []; 11 | for (let i = 0; i < totalPages; i++) { 12 | const start = collection.articlesPerPage * i; 13 | const end = start + collection.articlesPerPage; 14 | pages.push({ 15 | pageName: i === 0 && "index" || "page" + (i + 1), 16 | data: site.articlesCatalog.slice(start, end) 17 | }); 18 | } 19 | return pages; 20 | }; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/filters/categoryPagesArticlesFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return all the items in the catagoriesCatalog. 3 | */ 4 | module.exports = ({ site }) => 5 | site.categoriesCatalog.map(item => ({ pageName: item.category, data: item })); -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/about.md: -------------------------------------------------------------------------------- 1 | 7 | 8 |

    About

    9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-01-01-article1.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-02-01-article2.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-03-01-article3.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-04-16-article4.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-05-08-article5.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/articles/2019-06-30-article6.md: -------------------------------------------------------------------------------- 1 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 11 | 12 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 13 | 14 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. 15 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/blog/archive/index.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/blog/category/category.html: -------------------------------------------------------------------------------- 1 | 9 |

    Articles List

    10 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/blog/index.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/fragments/index.md: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | 10 |

    11 | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur lorem donec massa sapien. Sed egestas egestas fringilla phasellus faucibus. Pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id. 13 | 14 | Dolor morbi non arcu risus quis varius. Tincidunt praesent semper feugiat nibh sed. Pharetra diam sit amet nisl suscipit adipiscing bibendum est. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. In fermentum posuere urna nec. At risus viverra adipiscing at in tellus integer feugiat. 15 | 16 | Phasellus vestibulum lorem sed risus ultricies tristique nulla. Sagittis orci a scelerisque purus semper eget duis at tellus. Pellentesque elit eget gravida cum sociis. Nisl purus in mollis nunc sed id. 17 |
    -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/includes/header.html: -------------------------------------------------------------------------------- 1 | 4 |
    5 | 6 | 11 |
    -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_archivelink.scss: -------------------------------------------------------------------------------- 1 | .archive-link { 2 | text-decoration: none; 3 | font-size: 1.5rem; 4 | color: #000; 5 | 6 | &:hover { 7 | text-decoration: underline; 8 | } 9 | 10 | &-container { 11 | margin-top: 3rem; 12 | padding-top: 3rem; 13 | border-top: 1px solid #eee; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_archivepage.scss: -------------------------------------------------------------------------------- 1 | .archive { 2 | &__container { 3 | width: $blog-container-width; 4 | } 5 | 6 | &__title { 7 | font-size: 2.5rem; 8 | } 9 | 10 | &__list { 11 | list-style: none; 12 | 13 | &-item-link { 14 | text-decoration: none; 15 | font-size: 1.5rem; 16 | color: #000; 17 | 18 | &:hover { 19 | text-decoration: underline; 20 | } 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_articleimage.scss: -------------------------------------------------------------------------------- 1 | .article-image { 2 | display: block; 3 | width: 100%; 4 | 5 | &--pushed-down { 6 | margin-top: 3rem; 7 | } 8 | 9 | &-container { 10 | width: 100%; 11 | padding-top: 56.25%; // maintain 16:9 aspect ration 12 | position: relative; 13 | background-color: #eee; 14 | } 15 | 16 | &__text { 17 | position: absolute; 18 | top: calc(50% - (2.5ch * .5)); 19 | left: calc(50% - (5ch * .5)); 20 | font-size: 2.5rem; 21 | font-family: monospace; 22 | color: $image-mark-color; 23 | margin: 0; 24 | } 25 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_articlepage.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | margin-bottom: 2rem; 3 | 4 | &__title { 5 | font-size: 2.5rem; 6 | } 7 | 8 | &__date { 9 | font-size: 1.5rem; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_banner.scss: -------------------------------------------------------------------------------- 1 | .banner { 2 | width: 100vw; 3 | height: 30vh; 4 | background-color: #eee; 5 | display: flex; 6 | 7 | &__text { 8 | align-self: center; 9 | margin: 0 auto; 10 | font-size: 2.5rem; 11 | font-family: monospace; 12 | color: $image-mark-color; 13 | } 14 | } 15 | 16 | @media (min-width: 576px) { 17 | .banner { 18 | height: 50vh; 19 | } 20 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_blogpage.scss: -------------------------------------------------------------------------------- 1 | .blog { 2 | &__container { 3 | width: $blog-container-width; 4 | } 5 | 6 | &__articles { 7 | 8 | &-list { 9 | list-style: none; 10 | margin: 3rem 0 2rem 0; 11 | 12 | &:last-child { 13 | margin-bottom: 0; 14 | } 15 | 16 | &-item { 17 | margin-bottom: 3rem; 18 | 19 | &:last-child { 20 | margin-bottom: 0; 21 | } 22 | } 23 | } 24 | 25 | } 26 | 27 | &__article { 28 | &-title { 29 | margin-top: 1rem; 30 | font-size: 2.5rem; 31 | } 32 | 33 | &-link { 34 | color: inherit; 35 | text-decoration: none; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | } 40 | } 41 | 42 | &-date { 43 | font-size: 1.5rem; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_categorieslist.scss: -------------------------------------------------------------------------------- 1 | .categories { 2 | margin-top: 3rem; 3 | padding-top: 3rem; 4 | border-top: 1px solid #eee; 5 | 6 | &__title { 7 | margin-top: 0; 8 | font-size: 2rem; 9 | } 10 | 11 | &__list { 12 | list-style: none; 13 | 14 | &-item-link { 15 | text-decoration: none; 16 | font-size: 1.5rem; 17 | color: #000; 18 | 19 | &:hover { 20 | text-decoration: underline; 21 | } 22 | } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_categorypage.scss: -------------------------------------------------------------------------------- 1 | .category { 2 | &__container { 3 | width: $blog-container-width; 4 | } 5 | 6 | &__title { 7 | font-size: 2.5rem; 8 | } 9 | 10 | &__list { 11 | list-style: none; 12 | 13 | &-item-link { 14 | text-decoration: none; 15 | font-size: 1.5rem; 16 | color: #000; 17 | 18 | &:hover { 19 | text-decoration: underline; 20 | } 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_container.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: $mobile-container-width; 3 | margin: 0 auto; 4 | position: relative; 5 | } 6 | 7 | @media (min-width: 576px) { 8 | .container { 9 | max-width: $desktop-container-width; 10 | } 11 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | z-index: 100; 6 | width: 100%; 7 | padding: .5rem 0; 8 | background-color: $default-footer-bg-color; 9 | font-size: 1.2rem; 10 | 11 | &__container { 12 | text-align: center; 13 | } 14 | 15 | &__copy { 16 | max-width: 100%; 17 | color: $default-footer-text-color; 18 | font-size: .8rem; 19 | } 20 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: sticky; 3 | height: $header-height; 4 | padding: .5rem 0; 5 | line-height: calc(#{$header-height} - 1rem); 6 | background-color: $default-header-bg-color; 7 | top: 0; 8 | left: 0; 9 | z-index: 100; 10 | margin-bottom: $header-height; 11 | 12 | &--no-bottom-margin { 13 | margin-bottom: 0; 14 | } 15 | 16 | &__container { 17 | display: flex; 18 | flex-flow: row nowrap 19 | } 20 | 21 | &__brand { 22 | flex: 0 0 1; 23 | font-family: $serif-font; 24 | font-size: 1.5rem; 25 | font-weight: bold; 26 | color: #fff; 27 | margin: 0; 28 | text-decoration: none; 29 | } 30 | 31 | &__nav { 32 | flex: 0 0 1; 33 | display: flex; 34 | margin-left: auto; 35 | list-style: none; 36 | justify-self: flex-end; 37 | 38 | &-item { 39 | font-size: 1.1rem; 40 | margin: 0 .5rem; 41 | 42 | &-link { 43 | text-transform: uppercase; 44 | text-decoration: none; 45 | color: rgba(white, 0.8); 46 | &:hover { 47 | text-decoration: underline; 48 | } 49 | &--current { 50 | color: #fff; 51 | } 52 | } 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin full-width-margins($container-width) { 2 | margin: 0 calc(-100vw / 2 + #{$container-width} / 2); 3 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_navigator.scss: -------------------------------------------------------------------------------- 1 | .navigator { 2 | display: flex; 3 | width: 100%; 4 | margin-top: 4rem; 5 | 6 | %nav { 7 | flex: 50%; 8 | text-align: center; 9 | font-size: 1.3rem; 10 | padding: 1rem 0; 11 | border: 1px solid #eee; 12 | border-radius: $border-radius; 13 | background-color: #fff; 14 | } 15 | 16 | &__older, &__newer { 17 | @extend %nav; 18 | text-decoration: none; 19 | color: #00f; 20 | 21 | &:hover { 22 | background-color: #00f; 23 | color: #fff; 24 | } 25 | } 26 | 27 | &__empty { 28 | @extend %nav; 29 | color: rgba(#00f, .4); 30 | cursor: default; 31 | } 32 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_relatedarticleslist.scss: -------------------------------------------------------------------------------- 1 | .related-articles { 2 | margin-top: 3rem; 3 | padding-top: 3rem; 4 | border-top: 1px solid #eee; 5 | 6 | &__title { 7 | margin-top: 0; 8 | font-size: 2rem; 9 | } 10 | 11 | &__list { 12 | list-style: none; 13 | 14 | &-item-link { 15 | text-decoration: none; 16 | font-size: 1.5rem; 17 | color: #000; 18 | 19 | &:hover { 20 | text-decoration: underline; 21 | } 22 | } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_resets.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | border: 0; 6 | vertical-align: baseline; 7 | } 8 | 9 | body { 10 | font-family: $sans-serif-font; 11 | font-size: $default-font-size; 12 | color: $default-text-color; 13 | background-color: $default-bg-color; 14 | margin-bottom: 8rem; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6 { 23 | color: #00f; 24 | font-family: 'Times New Roman', Times, serif; 25 | margin: 1rem 0; 26 | } 27 | 28 | h1 { 29 | font-size: 3.333rem; 30 | } 31 | 32 | h2 { 33 | font-size: 3rem; 34 | } 35 | 36 | h3 { 37 | font-size: 2.666rem; 38 | } 39 | 40 | h4 { 41 | font-size: 2.333rem; 42 | } 43 | 44 | h5 { 45 | font-size: 2rem; 46 | } 47 | 48 | h6 { 49 | font-size: 1.666rem; 50 | } 51 | 52 | p { 53 | margin: .5rem 0; 54 | letter-spacing: 1px; 55 | font-size: 1.35rem; 56 | line-height: $default-line-height; 57 | color: $default-text-color-subdued; 58 | max-width: 45rem; 59 | font-size: 1.4rem; 60 | } -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $default-text-color: #000; 2 | $default-text-color-subdued: rgba(#000, .58); 3 | $default-bg-color: #fff; 4 | $default-font-size: 16px; 5 | $default-line-height: 140%; 6 | $default-header-bg-color: rgb(43, 62, 231); 7 | $default-footer-bg-color: rgb(232, 234, 248); 8 | $default-footer-text-color: rgb(43, 62, 231); 9 | 10 | $header-height: 4rem; 11 | $desktop-container-width: 75vw; 12 | $mobile-container-width: 90vw; 13 | $blog-container-width: 40rem; 14 | $blog-max-image-width: 40rem; 15 | $blog-max-image-height: calc(40rem * .5625); 16 | $sans-serif-font: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 17 | $serif-font: 'Times New Roman', Times, serif; 18 | $image-mark-color: rgba(#000, 0.15); 19 | $border-radius: 4px; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "resets"; 3 | @import "mixins"; 4 | @import "container"; 5 | @import "footer"; 6 | @import "header"; 7 | @import "navigator"; 8 | @import "banner"; 9 | @import "articleimage"; 10 | @import "categorieslist"; 11 | @import "blogpage"; 12 | @import "categorypage"; 13 | @import "articlepage"; 14 | @import "archivepage"; 15 | @import "archivelink"; 16 | @import "relatedarticleslist"; -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    12 |
    13 | 16 |
    17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/archivepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    12 |
    13 | 16 |
    17 |

    Archive

    18 | 19 |
    20 |
    21 | 22 | 23 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/articlepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    13 |
    14 | 17 |
    18 |
    19 |

    Image

    20 |
    21 |

    22 | 23 |
    24 |
    25 | 29 |
    30 |

    Categories

    31 | 32 |
    33 | 36 | 37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/blogpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    13 |
    14 | 17 |
    18 | 19 |
    20 |
    21 |

    Categories

    22 | 23 |
    24 | 27 | 28 |
    29 | 30 | 31 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/categorypage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    12 |
    13 | 16 |
    17 |
    18 | 21 |
    22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/source/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
    12 |
    13 | 16 |
    17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /scaffolding/reference-project-1/trio.json: -------------------------------------------------------------------------------- 1 | { 2 | "blogFolderName": "blog", 3 | "baseUrl": "", 4 | "permalinks": [ 5 | "articles: blog" 6 | ] 7 | } 8 | --------------------------------------------------------------------------------