├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── LICENSE.md ├── README.md ├── docs └── images │ └── front.png ├── ecosystem.config.js ├── package-lock.json ├── package.json ├── preact.config.js ├── prerender-urls.json ├── server ├── api.js ├── index.js └── utils.js ├── src ├── assets │ ├── favicon.ico │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icon-256x256.png │ │ ├── icon-384x384.png │ │ ├── icon.svg │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ ├── logo.svg │ ├── logo@1x.png │ ├── logo@2x.png │ ├── logo@3x.png │ └── swap.svg ├── components │ ├── app.js │ ├── backlink │ │ └── index.js │ ├── default │ │ ├── sectioncontent │ │ │ └── index.js │ │ └── tablerows │ │ │ └── index.js │ ├── footer │ │ └── index.js │ ├── hamburger │ │ └── index.js │ ├── header │ │ └── index.js │ ├── icons │ │ ├── can │ │ │ └── index.js │ │ ├── cant │ │ │ └── index.js │ │ ├── contrast │ │ │ └── index.js │ │ ├── doubt │ │ │ └── index.js │ │ ├── leftarrow │ │ │ └── index.js │ │ ├── moon │ │ │ └── index.js │ │ ├── settings │ │ │ └── index.js │ │ ├── sun │ │ │ └── index.js │ │ └── swap │ │ │ └── index.js │ ├── logo │ │ └── index.js │ ├── resultmenuitem │ │ └── index.js │ ├── search │ │ └── index.js │ ├── swaptags │ │ └── index.js │ ├── tagsettings │ │ └── index.js │ └── themeswitcher │ │ └── index.js ├── index.js ├── manifest.json ├── routes │ ├── main │ │ └── index.js │ └── result │ │ └── index.js ├── static │ └── robots.txt ├── style │ └── index.css ├── sw.js └── template.html ├── tailwind.config.js ├── tests ├── __mocks__ │ ├── browserMocks.js │ ├── fileMocks.js │ └── setupTests.js └── header.test.js └── utils ├── archive.js └── wget.js /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/javascript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster 4 | ARG VARIANT="16-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node modules 16 | RUN su node -c "npm install -g eslint preact-cli nodemon concurrently" 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/javascript-node 3 | { 4 | "name": "caninclude preact", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 16, 14, 12. 8 | // Append -bullseye or -buster to pin to an OS version. 9 | // Use -bullseye variants on local arm64/Apple Silicon. 10 | "args": { "VARIANT": "16-bullseye" } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "dbaeumer.vscode-eslint", 19 | "bradlc.vscode-tailwindcss", 20 | "eamodio.gitlens" 21 | ], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | "postCreateCommand": "npm i", 28 | 29 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 30 | "remoteUser": "node" 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /*.log 4 | size-plugin.json 5 | server/spec.json 6 | server/params.json 7 | 8 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2021 Aleksandr Vishniakov aka CyberLight. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caninclude 2 | 3 | ## About 4 |
5 | 6 |
7 | 8 | Please, see **Demo** [here](https://caninclude.glitch.me) 9 | 10 | This is the second generation of the caninclude service! 11 | 12 | ## Development environment 13 | * [VSCode](https://code.visualstudio.com/) 14 | * [VSCode Remote Containers](https://code.visualstudio.com/docs/remote/containers#_installation) 15 | * Clone `git clone https://github.com/CyberLight/caninclude-v2` 16 | * Go to cloned project repo folder `cd caninclude` 17 | * Open in VSCode `code .` 18 | * In popup menu click by `Reopen in Container` 19 | * Whew! 20 | 21 | ## CLI Commands 22 | 23 | ``` bash 24 | # serve web part with hot reload at localhost:8080 and api part on localhost:8081 25 | # with proxying traffic from /api to web /api to localhost:8081 26 | npm run dev 27 | 28 | # build for production with minification 29 | npm run build 30 | 31 | # test the production build locally 32 | npm run serve 33 | 34 | # launches linting of the whole project 35 | npm run lint 36 | 37 | # launches only api:dev server at localhost:8081 38 | api:dev 39 | ``` 40 | 41 | For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md). -------------------------------------------------------------------------------- /docs/images/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/docs/images/front.png -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: "webapp", 4 | script: "./server/index.js", 5 | instances: 3, 6 | exec_mode: "cluster", 7 | max_memory_restart: '300M', 8 | time: true, 9 | env_production: { 10 | NODE_ENV: "production", 11 | } 12 | }] 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "caninclude", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "pm2 start ecosystem.config.js --env production --no-daemon", 8 | "build": "preact build", 9 | "serve": "sirv build --port 8080 --cors --single", 10 | "dev": "concurrently --kill-others \"preact watch\" \"npm run api:dev\"", 11 | "lint": "eslint src", 12 | "test": "jest", 13 | "api:dev": "PORT=8081 nodemon --watch server server/index.js", 14 | "pm2:gen:config": "pm2 ecosystem", 15 | "pm2:stop": "pm2 stop ecosystem.config.js", 16 | "pm2:restart": "pm2 restart ecosystem.config.js --env production", 17 | "pm2:reload": "pm2 reload ecosystem.config.js --env production", 18 | "pm2:mon": "pm2 monit", 19 | "pm2:kill": "pm2 kill", 20 | "pm2:logs": "pm2 logs webapp --lines 100", 21 | "pm2:log:rotate": "pm2 install pm2-logrotate", 22 | "pm2:flush": "pm2 flush", 23 | "glitch:pack": "npm run build && node ./utils/archive.js", 24 | "glitch:unpack": "rm -rf build/* && unzip -o glitch_release_*.zip -d . && rm glitch_release_*.zip && refresh", 25 | "glitch:apply": "refresh", 26 | "glitch:wget": "node ./utils/wget" 27 | }, 28 | "author": "Aleksandr Vishniakov aka CyberLight", 29 | "engines": { 30 | "node": "16.x" 31 | }, 32 | "eslintConfig": { 33 | "extends": "preact", 34 | "ignorePatterns": [ 35 | "build/" 36 | ], 37 | "root": true 38 | }, 39 | "devDependencies": { 40 | "archiver": "^5.3.0", 41 | "enzyme": "^3.10.0", 42 | "enzyme-adapter-preact-pure": "^2.0.0", 43 | "eslint": "^8.2.0", 44 | "eslint-config-preact": "^1.2.0", 45 | "eslint-plugin-compat": "^4.0.0", 46 | "jest": "^27.3.1", 47 | "jest-preset-preact": "^1.0.0", 48 | "pino-http": "^6.5.0", 49 | "preact-cli": "^0.1.0", 50 | "sirv-cli": "1.0.3" 51 | }, 52 | "dependencies": { 53 | "@polka/send-type": "^0.5.2", 54 | "caninclude-analyzer": "github:CyberLight/caninclude-analyzer#v1.0.8", 55 | "lodash": "^4.17.21", 56 | "pm2": "^5.1.2", 57 | "polka": "^0.5.2", 58 | "preact": "^10.6.4", 59 | "preact-render-to-string": "^5.1.19", 60 | "preact-router": "^4.0.1", 61 | "sirv": "^2.0.2", 62 | "tailwindcss": "^3.0.15" 63 | }, 64 | "jest": { 65 | "preset": "jest-preset-preact", 66 | "setupFiles": [ 67 | "/tests/__mocks__/browserMocks.js", 68 | "/tests/__mocks__/setupTests.js" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /preact.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, env, helpers, /* options */) => { 2 | const postCssLoaders = helpers.getLoadersByName(config, 'postcss-loader'); 3 | postCssLoaders.forEach(({ loader }) => { 4 | if (!loader.options.postcssOptions.plugins) { 5 | loader.options.postcssOptions.plugins = []; 6 | } 7 | const plugins = loader.options.postcssOptions.plugins; 8 | 9 | // Add tailwind css at the top. 10 | plugins.unshift(require('tailwindcss')); 11 | }); 12 | 13 | if (!env.isProd) { 14 | config.devServer.proxy = [ 15 | { 16 | path: '/api/**', 17 | target: 'http://localhost:8081', 18 | }, 19 | { 20 | path: '/api', 21 | target: 'http://localhost:8081', 22 | } 23 | ]; 24 | } 25 | if (env.isProd) { 26 | config.devtool = false; // disable sourcemaps 27 | } 28 | return config; 29 | }; -------------------------------------------------------------------------------- /prerender-urls.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "/", 4 | "title": "Caninclude" 5 | }, 6 | { 7 | "url": "/caninclude", 8 | "title": "Result" 9 | } 10 | ] -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | const get = require('lodash/get'); 2 | const send = require('@polka/send-type'); 3 | const spec = require('./spec.json'); 4 | const params = require('./params.json'); 5 | const {CanincludeAnalyzer, rules} = require('caninclude-analyzer'); 6 | const {copyObject} = require('./utils'); 7 | const analyzer = new CanincludeAnalyzer(rules); 8 | 9 | const resultsMap = { 10 | true: 'can', 11 | false: 'cant', 12 | unknown: 'doubt' 13 | } 14 | 15 | module.exports = { 16 | caninclude: (req, res) => { 17 | const { child, parent, childParams, parentParams } = req.query; 18 | const requiredQueryParams = [child, parent]; 19 | 20 | if (!requiredQueryParams.every(Boolean)) { 21 | const data = { ok: false, message: 'Please set "child" and "parent" parameters', type: 'warning' }; 22 | return send(res, 400, data); 23 | } 24 | 25 | const childTagFormatted = child.toLowerCase(); 26 | const parentTagFormatted = parent.toLowerCase(); 27 | const targetTags = [childTagFormatted, parentTagFormatted]; 28 | 29 | const filteredTags = spec 30 | .filter((tag) => targetTags.includes(tag.tag)); 31 | 32 | if (filteredTags.length < 2 && childTagFormatted !== parentTagFormatted) { 33 | const data = { ok: false, message: 'Some values from parameters were not found', type: 'warning' }; 34 | return send(res, 400, data); 35 | } 36 | 37 | const tags = filteredTags.reduce((o, tag) => ({[tag.tag]: tag, ...o}), {}); 38 | const result = { child: copyObject(tags[childTagFormatted]), parent: copyObject(tags[parentTagFormatted]) }; 39 | result.child.params = copyObject(params[childTagFormatted].params); 40 | result.parent.params = copyObject(params[parentTagFormatted].params); 41 | const childParamsList = get(result, 'child.params.Categories', []); 42 | const parentParamsList = get(result, 'parent.params.ContentModel', []); 43 | 44 | const toObject = (distinct, line) => ({[line.hashText]: line, ...distinct}); 45 | const onlyMatchedHref = (paramsList) => (line) => typeof line !== 'string' && paramsList.includes(line.hashText); 46 | 47 | const childParamList = result.child.Categories 48 | .flatMap((block) => block.filter(onlyMatchedHref(childParamsList))).reduce(toObject, {}); 49 | 50 | const parentParamList = result.parent.ContentModel 51 | .flatMap((block) => block.filter(onlyMatchedHref(parentParamsList))).reduce(toObject, {}); 52 | 53 | result.child.params = Object.values(childParamList); 54 | result.parent.params = Object.values(parentParamList); 55 | 56 | result.include = analyzer.canInclude( 57 | { name: childTagFormatted, params: childParams }, 58 | { name: parentTagFormatted, params: parentParams }, 59 | true 60 | ); 61 | 62 | result.include.can = resultsMap[result.include.can]; 63 | if (result.include.alternative) { 64 | result.include.alternative = { 65 | ...result.include.alternative, 66 | can: resultsMap[result.include.alternative.can], 67 | }; 68 | } 69 | const data = { ok: true, result }; 70 | send(res, 200, data); 71 | } 72 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka'); 2 | const send = require('@polka/send-type'); 3 | const logger = require('pino-http')() 4 | const { PORT=8080 } = process.env; 5 | 6 | const isProd = process.env.NODE_ENV === 'production'; 7 | const staticMiddleware = require('sirv')('build', { 8 | single: true, 9 | dev: !isProd 10 | }); 11 | const { caninclude } = require('./api.js'); 12 | 13 | function onError(err, req, res) { 14 | console.error(`> ERROR: ${req.method}(${req.url}) ~>`, err); 15 | const data = { ok: false, message: 'Oops! Something went wrong!', type: 'warning' }; 16 | send(res, 500, data) 17 | } 18 | 19 | let app = polka({ onError }); 20 | app.use((req, res, next) => { 21 | logger(req, res); 22 | next(); 23 | }) 24 | app.use((req, res, next) => { 25 | if (!req.headers['x-forwarded-proto'] || req.headers['x-forwarded-proto'].indexOf('https') !== -1) { 26 | return next(); 27 | } 28 | const url = `https://${req.headers.host}${req.url}`; 29 | let str = `Redirecting to ${url}`; 30 | res.writeHead(301, { 31 | Location: url, 32 | 'Content-Type': 'text/plain', 33 | 'Content-Length': str.length 34 | }); 35 | res.end(str); 36 | }) 37 | app.use((req, res, next) => { 38 | try { 39 | if (req.path === '/api/caninclude' && req.method === 'GET') { 40 | return caninclude(req, res); 41 | } 42 | } catch (e) { 43 | return next(e); 44 | } 45 | return next(); 46 | }) 47 | if (isProd) { 48 | app.use(staticMiddleware); 49 | } 50 | app.listen(PORT, () => { 51 | console.log(`> Running on localhost:${PORT}`); 52 | }); -------------------------------------------------------------------------------- /server/utils.js: -------------------------------------------------------------------------------- 1 | function copyObject(o) { 2 | return {...o}; 3 | } 4 | 5 | module.exports = { 6 | copyObject 7 | } -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/icon-256x256.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/logo@1x.png -------------------------------------------------------------------------------- /src/assets/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/logo@2x.png -------------------------------------------------------------------------------- /src/assets/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberLight/caninclude-v2/214e59110ec58eb81b6bc45787c95fa6722675ce/src/assets/logo@3x.png -------------------------------------------------------------------------------- /src/assets/swap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Router } from 'preact-router'; 3 | import Header from './header'; 4 | import Footer from './footer'; 5 | 6 | // Code-splitting is automated for `routes` directory 7 | import Main from '../routes/main'; 8 | import Result from '../routes/result'; 9 | 10 | const App = () => { 11 | return ( 12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/backlink/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | import LeftArrowIcon from '../icons/leftarrow'; 4 | 5 | const BackLink = ({ onClick }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default BackLink; -------------------------------------------------------------------------------- /src/components/default/sectioncontent/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const DefaultSectionsContent = () => ( 4 | <> 5 |
  • 6 |
  • 7 |
  • 8 | 9 | ); 10 | 11 | export default DefaultSectionsContent; -------------------------------------------------------------------------------- /src/components/default/tablerows/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const DefaultTableRows = () => 4 | new Array(10).fill('').map((_, index) => ( 5 | 6 |
    7 |
    8 |
    9 |
    10 | 11 | ) 12 | ); 13 | 14 | export default DefaultTableRows; -------------------------------------------------------------------------------- /src/components/footer/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const Footer = () => ( 4 | 22 | ); 23 | 24 | export default Footer; -------------------------------------------------------------------------------- /src/components/hamburger/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const Hamburger = ({ onClick, closed }) => ( 4 | 20 | ); 21 | 22 | export default Hamburger; -------------------------------------------------------------------------------- /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Match, { Link } from 'preact-router/match'; 3 | import { useState, useEffect } from 'preact/hooks'; 4 | import Hamburger from '../hamburger'; 5 | import ThemeSwitcher from '../themeswitcher'; 6 | import ResultMenuItem from '../resultmenuitem'; 7 | import BackLink from "../backlink"; 8 | 9 | const Header = () => { 10 | const [menuClosed, setMenuClosed] = useState(undefined); 11 | const onMenuBtnClick = (e) => setMenuClosed(!e.target.checked); 12 | const onMenuItemClick = () => setMenuClosed(true); 13 | const isMenuInitialized = typeof menuClosed !== 'undefined'; 14 | 15 | useEffect(() => { 16 | setMenuClosed(true) 17 | }, []); 18 | 19 | return ( 20 |
    21 | 59 |
    v 2.0
    60 |
    61 | ); 62 | }; 63 | 64 | export default Header; 65 | -------------------------------------------------------------------------------- /src/components/icons/can/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const CanIcon = () => ( 4 | 5 | 6 | 7 | ) 8 | 9 | export default CanIcon; -------------------------------------------------------------------------------- /src/components/icons/cant/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const CantIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default CantIcon; -------------------------------------------------------------------------------- /src/components/icons/contrast/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const ContrastIcon = (props) => ( 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default ContrastIcon; 15 | -------------------------------------------------------------------------------- /src/components/icons/doubt/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const DoubtIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default DoubtIcon; -------------------------------------------------------------------------------- /src/components/icons/leftarrow/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const LeftArrowIcon = () => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default LeftArrowIcon; 15 | -------------------------------------------------------------------------------- /src/components/icons/moon/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const MoonIcon = (props) => ( 4 | 5 | 6 | 8 | 10 | 12 | 13 | 14 | ); 15 | 16 | export default MoonIcon; -------------------------------------------------------------------------------- /src/components/icons/settings/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const SettingsIcon = () => ( 4 | 5 | 6 | 29 | 31 | 32 | 33 | ); 34 | 35 | export default SettingsIcon; -------------------------------------------------------------------------------- /src/components/icons/sun/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const SunIcon = (props) => ( 4 | 5 | 6 | 8 | 20 | 21 | 22 | ); 23 | 24 | export default SunIcon; -------------------------------------------------------------------------------- /src/components/icons/swap/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const SwapIcon = (props) => ( 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default SwapIcon; -------------------------------------------------------------------------------- /src/components/logo/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const Logo = () => { 4 | return ( 5 |
    6 | 7 | 8 | 9 | caninclude logo 16 | 17 |
    18 | ); 19 | } 20 | 21 | export default Logo; -------------------------------------------------------------------------------- /src/components/resultmenuitem/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | 4 | const ResultMenuItem = ({ onClick, href }) => { 5 | return ( 6 |
  • 7 | Result 12 |
  • 13 | ); 14 | } 15 | 16 | export default ResultMenuItem; -------------------------------------------------------------------------------- /src/components/search/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | import { route } from 'preact-router'; 4 | import Swap from '../icons/swap'; 5 | 6 | const Search = () => { 7 | const [swap, setSwap] = useState(false); 8 | const [formData, setFormData] = useState({ child: '', parent: '' }) 9 | const swapClick = () => { 10 | const { child: parent, parent: child } = formData || {}; 11 | setFormData({ parent, child }); 12 | setSwap(!swap); 13 | }; 14 | const onChildInput = (e) => setFormData({ ...formData, child: e.target.value }); 15 | const onParentInput = (e) => setFormData({ ...formData, parent: e.target.value }); 16 | const onSubmit = (e) => { 17 | e.preventDefault(); 18 | const formData = new FormData(e.target); 19 | route(`/caninclude?${new URLSearchParams(formData)}`); 20 | }; 21 | 22 | return ( 23 |
    24 |
    25 |

    26 | 27 | 37 |

    38 |

    39 | 40 | 45 |

    46 |

    47 | 48 | 57 |

    58 |

    59 | 60 |

    61 |
    62 |
    63 | ); 64 | }; 65 | 66 | export default Search; 67 | -------------------------------------------------------------------------------- /src/components/swaptags/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import SwapIcon from '../icons/swap'; 3 | 4 | const SwapTags = (props) => { 5 | return ( 6 | 7 | 8 | Swap tags 9 | 10 | ); 11 | }; 12 | 13 | export default SwapTags; -------------------------------------------------------------------------------- /src/components/tagsettings/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import SettingsIcon from '../icons/settings'; 3 | 4 | const mapLine = (line, loading, clickHandler) => ( 5 | 6 | {line.text} 7 | 12 | 13 | ); 14 | 15 | const TagSettings = ({ id, loading=false, lines=[], clickHandler } = {}) => ( 16 |
    17 | 18 | 21 | 24 |
    25 | ); 26 | 27 | export default TagSettings; -------------------------------------------------------------------------------- /src/components/themeswitcher/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState, useEffect } from 'preact/hooks'; 3 | import MoonIcon from '../icons/moon'; 4 | import SunIcon from '../icons/sun'; 5 | import ContrastIcon from '../icons/contrast'; 6 | 7 | const ThemeSwitcher = (props) => { 8 | const [theme, setTheme] = useState('auto'); 9 | 10 | useEffect(() => { 11 | if (typeof window !== 'undefined') { 12 | if ('theme' in localStorage) { 13 | setTheme(window.localStorage.theme); 14 | } else { 15 | setTheme('auto'); 16 | } 17 | } 18 | }, [setTheme]); 19 | 20 | useEffect(() => { 21 | if (typeof window !== 'undefined') { 22 | if (theme === 'auto') { 23 | window.localStorage.removeItem('theme'); 24 | } else { 25 | window.localStorage.theme = theme; 26 | } 27 | if (localStorage.theme === 'dark' || 28 | (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 29 | document.documentElement.classList.add('dark') 30 | } else { 31 | document.documentElement.classList.remove('dark') 32 | } 33 | } 34 | }, [theme]); 35 | 36 | const onClickTheme = (e) => { 37 | setTheme(e.target.value); 38 | } 39 | 40 | return ( 41 |
      42 |
    • 43 | 44 |
      45 | 46 |
      47 |
    • 48 |
    • 49 | 50 |
      51 | 52 |
      53 |
    • 54 |
    • 55 | 56 |
      57 | 58 |
      59 |
    • 60 |
    61 | ); 62 | } 63 | 64 | export default ThemeSwitcher; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import App from './components/app'; 3 | 4 | export default App; 5 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caninclude", 3 | "short_name": "caninclude", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#673ab8", 8 | "theme_color": "#673ab8", 9 | "description": "Can you include the child tag in the parent tag?", 10 | "icons": [ 11 | { 12 | "src": "/assets/icons/icon.svg", 13 | "type": "image/svg+xml", 14 | "sizes": "512x512" 15 | }, 16 | { 17 | "src": "/assets/icons/android-chrome-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/assets/icons/icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/assets/icons/icon-256x256.png", 28 | "sizes": "256x256", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "/assets/icons/android-chrome-192x192.png", 33 | "sizes": "192x192", 34 | "type": "image/png" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/routes/main/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Search from '../../components/search' 3 | import Logo from '../../components/logo' 4 | 5 | const Main = () => ( 6 |
    7 |

    Can I include

    8 |
    9 |

    Welcome

    10 | 11 |
    12 |
    13 |

    Can I include a child tag to a parent tag?

    14 | 15 |
    16 |
    17 | ); 18 | 19 | export default Main; 20 | -------------------------------------------------------------------------------- /src/routes/result/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState, useEffect } from 'preact/hooks'; 3 | import get from 'lodash/get'; 4 | import { route } from 'preact-router'; 5 | 6 | import CanIcon from '../../components/icons/can'; 7 | import CantIcon from '../../components/icons/cant'; 8 | import DoubtIcon from '../../components/icons/doubt'; 9 | import TagSettings from '../../components/tagsettings'; 10 | import SwapTags from '../../components/swaptags'; 11 | 12 | import DefaultSectionsContent from '../../components/default/sectioncontent'; 13 | import DefaultTableRows from '../../components/default/tablerows'; 14 | 15 | const CanIconType = 'can'; 16 | const CantIconType = 'cant'; 17 | const DoubtIconType = 'doubt'; 18 | 19 | 20 | const Result = ({ matches: { child, parent } = {} }) => { 21 | const [result, setResult] = useState(); 22 | const [childSelectedParams, setChildSelectedParams] = useState([]); 23 | const [parentSelectedParams, setParentSelectedParams] = useState([]); 24 | const [loading, setLoading] = useState(true); 25 | const [error, setError] = useState(null); 26 | 27 | const childSelectParamHandler = (e) => { 28 | if (e.target.checked) { 29 | setChildSelectedParams([...childSelectedParams, `is:${e.target.name}`]); 30 | } else { 31 | setChildSelectedParams(childSelectedParams.filter((param) => param !== `is:${e.target.name}`)); 32 | } 33 | }; 34 | 35 | const parentSelectParamHandler = (e) => { 36 | if (e.target.checked) { 37 | setParentSelectedParams([...parentSelectedParams, `is:${e.target.name}`]); 38 | } else { 39 | setParentSelectedParams(parentSelectedParams.filter((param) => param !== `is:${e.target.name}`)); 40 | } 41 | }; 42 | 43 | useEffect(() => { 44 | const params = new URLSearchParams({ child, parent, childParams: childSelectedParams, parentParams: parentSelectedParams }); 45 | async function checkResponse(res) { 46 | if (!res.ok) { 47 | let err = new Error(`HTTP Error: ${res.status}`); 48 | err.json = await res.json(); 49 | err.status = res.status 50 | throw err 51 | } 52 | return res; 53 | } 54 | fetch(`/api/caninclude?${params}`) 55 | .then((r) => checkResponse(r)) 56 | .then((r) => r.json()) 57 | .then((json) => setResult(json.result)) 58 | .catch((e) => setError(e)) 59 | .finally(() => setLoading(false)); 60 | }, [child, parent, childSelectedParams, parentSelectedParams]); 61 | 62 | const redirectToMainPage = (e) => { 63 | if (e.stopImmediatePropagation) { 64 | e.stopImmediatePropagation(); 65 | } 66 | if (e.stopPropagation) { 67 | e.stopPropagation(); 68 | } 69 | e.preventDefault(); 70 | route('/'); 71 | } 72 | 73 | const childTag = get(result, 'child.tag', 'child'); 74 | const childParams = get(result, 'child.params', []); 75 | const parentTag = get(result, 'parent.tag', `parent`); 76 | const parentParams = get(result, 'parent.params', []); 77 | const alternativeIconType = get(result, 'include.alternative.can'); 78 | const iconType = alternativeIconType || get(result, 'include.can'); 79 | const alternativeMessage = get(result, 'include.alternative.message', []); 80 | const includeParams = get(result, 'include.params', []).reduce((o, [key, props]) => ({[key]: props, ...o}), {}); 81 | const childCategories = get(result, 'child.Categories', []); 82 | const childContextUsed = get(result, 'child.ContextsInWhichThisElementCanBeUsed', []); 83 | const childContentModel = get(result, 'child.ContentModel', []); 84 | const parentCategories = get(result, 'parent.Categories', []); 85 | const parentContextUsed = get(result, 'parent.ContextsInWhichThisElementCanBeUsed', []); 86 | const parentContentModel = get(result, 'parent.ContentModel', []); 87 | const parentSupport = Object.entries(get(result, 'parent.support', {})); 88 | const childSupport = Object.entries(get(result, 'child.support', {})); 89 | const jsonError = get(error, 'json'); 90 | const hasFatalError = error && !jsonError; 91 | 92 | const mapLink = (line, selectParams) => { 93 | const params = selectParams[line.hashText]; 94 | return params 95 | ? ( 96 | {line.text} 100 | {params.priority} 101 | ) 102 | : ({line.text}); 105 | } 106 | 107 | const mapBlock = (block, selectParams = {}) => { 108 | return block.map((line) => { 109 | if (typeof line !== 'string') { 110 | return mapLink(line, selectParams); 111 | } 112 | return line; 113 | }) 114 | } 115 | 116 | return ( 117 |
    118 |

    Result of including a tag in a tag

    119 | 124 | {alternativeMessage.length > 0 && 125 | (
    126 | 127 |
    {alternativeMessage.join(' ')}
    128 |
    129 | 130 |
    ) 131 | } 132 | {jsonError && ( 133 |
    134 | {jsonError.message}|Go to main page 135 |
    136 | )} 137 | {hasFatalError && ( 138 |
    139 | :-( Something went wrong... Try again later!|Go to main page 140 |
    141 | )} 142 |
    143 | 144 | 145 |
    146 | 147 | 148 | 149 |
    150 | 200 |
    201 |

    Can include?

    202 |
    203 | { iconType === CanIconType && } 204 | { iconType === CantIconType && } 205 | { iconType === DoubtIconType && } 206 | { !iconType &&
    } 207 |
    208 | { iconType === CanIconType && Yes, you can! } 209 | { iconType === CantIconType && No, you can't! } 210 | { iconType === DoubtIconType && Doubt?! } 211 | { !iconType && } 212 |
    214 |
    215 |

    tag: {`<${parentTag}/>`}

    216 |
    217 |

    Categories

    218 |
      219 | {!parentCategories.length ? : parentCategories.map((block, index) => (
    • {mapBlock(block)}
    • ))} 220 |
    221 |
    222 |
    223 |

    Contexts in which this element can be used

    224 |
      225 | {!parentContextUsed.length ? : parentContextUsed.map((block, index) => (
    • {mapBlock(block)}
    • ))} 226 |
    227 |
    228 |
    229 |

    Content model

    230 | {parentParams.length > 0 && 231 | } 237 |
      238 | {!parentContentModel.length ? : parentContentModel.map((block, index) => (
    • {mapBlock(block, includeParams)}
    • ))} 239 |
    240 |
    241 |
    242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | { !parentSupport.length ? : parentSupport.map(([browser, params], index) => { 253 | const row = [browser].concat(Object.values(params)); 254 | return { 255 | row.map((col, index) => 256 | 257 | ) 258 | } 259 | })} 260 | 261 |
    BrowserWeb HTMLWeb APICanIUse
    {col}
    262 |
    263 |
    264 |
    265 |
    266 | ); 267 | } 268 | 269 | export default Result; 270 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/sw.js: -------------------------------------------------------------------------------- 1 | import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/'; 2 | import { registerRoute } from 'workbox-routing'; 3 | import { StaleWhileRevalidate } from 'workbox-strategies'; 4 | 5 | if ('serviceWorker' in navigator) { 6 | navigator.serviceWorker.getRegistrations() 7 | .then((registrations) => { 8 | for(let registration of registrations) { 9 | registration.unregister(); 10 | } 11 | }); 12 | } 13 | 14 | registerRoute( 15 | /\/api\/.*/, 16 | new StaleWhileRevalidate(), 17 | 'GET' 18 | ); 19 | 20 | setupRouting(); 21 | setupPrecaching(getFiles()); 22 | -------------------------------------------------------------------------------- /src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% preact.title %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | 33 | <% preact.headEnd %> 34 | 35 | 36 | <% preact.bodyEnd %> 37 | 38 | 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | 3 | module.exports = { 4 | mode: 'jit', 5 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './src/template.html'], 6 | darkMode: 'class', // or 'media' or 'class' 7 | theme: { 8 | extend: { 9 | backgroundSize: { 10 | 'full-h-3': '100% .3rem' 11 | }, 12 | backgroundImage: { 13 | swap: 'url(/assets/swap.svg)', 14 | logo: 'url(/assets/logo.svg)' 15 | }, 16 | boxShadow: { 17 | 'inset-thin': 'inset 0 2px 5px rgba(0,0,0,.2)' 18 | }, 19 | minHeight: { 20 | content: 'calc(100vh - 3.5rem - 3.5rem)', 21 | screen: '100vh' 22 | }, 23 | maxWidth: { 24 | '1/2': '50%', 25 | '1/3': '33.33%' 26 | } 27 | } 28 | }, 29 | variants: { 30 | extend: {}, 31 | }, 32 | plugins: [ 33 | plugin(({ addVariant, e }) => { 34 | addVariant('peer-checked-fch', ({ modifySelectors, separator }) => { 35 | modifySelectors(({ className }) => { 36 | return `.peer:checked ~ .${e(`peer-checked-fch${separator}${className}`)} > :first-child` 37 | }) 38 | }), 39 | addVariant('peer-not-checked-lch', ({ modifySelectors, separator }) => { 40 | modifySelectors(({ className }) => { 41 | return `.peer:not(:checked) ~ .${e(`peer-not-checked-lch${separator}${className}`)} > :last-child` 42 | }) 43 | }) 44 | }) 45 | ], 46 | } 47 | -------------------------------------------------------------------------------- /tests/__mocks__/browserMocks.js: -------------------------------------------------------------------------------- 1 | // Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage 2 | /** 3 | * An example how to mock localStorage is given below 👇 4 | */ 5 | 6 | /* 7 | // Mocks localStorage 8 | const localStorageMock = (function() { 9 | let store = {}; 10 | 11 | return { 12 | getItem: (key) => store[key] || null, 13 | setItem: (key, value) => store[key] = value.toString(), 14 | clear: () => store = {} 15 | }; 16 | 17 | })(); 18 | 19 | Object.defineProperty(window, 'localStorage', { 20 | value: localStorageMock 21 | }); */ 22 | -------------------------------------------------------------------------------- /tests/__mocks__/fileMocks.js: -------------------------------------------------------------------------------- 1 | // This fixed an error related to the CSS and loading gif breaking my Jest test 2 | // See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets 3 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /tests/__mocks__/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-preact-pure'; 3 | 4 | configure({ 5 | adapter: new Adapter() 6 | }); 7 | -------------------------------------------------------------------------------- /tests/header.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Header from '../src/components/header'; 3 | // See: https://github.com/preactjs/enzyme-adapter-preact-pure 4 | import { shallow } from 'enzyme'; 5 | 6 | describe('Initial Test of the Header', () => { 7 | test('Header renders 3 nav items', () => { 8 | const context = shallow(
    ); 9 | expect(context.find('h1').text()).toBe('Preact App'); 10 | expect(context.find('Link').length).toBe(3); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /utils/archive.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const archiver = require('archiver'); 3 | const output = fs.createWriteStream(`glitch_release_${+new Date()}.zip`); 4 | const archive = archiver('zip', { 5 | zlib: { level: 9 } // Sets the compression level. 6 | }); 7 | 8 | output.on('close', () => { 9 | // eslint-disable-next-line no-console 10 | console.log(`${archive.pointer()} total bytes`); 11 | // eslint-disable-next-line no-console 12 | console.log('archiver has been finalized and the output file descriptor has closed.'); 13 | }); 14 | 15 | output.on('end', () => { 16 | // eslint-disable-next-line no-console 17 | console.log('Data has been drained'); 18 | }); 19 | 20 | archive.on('warning', (err) => { 21 | if (err.code === 'ENOENT') { 22 | // log warning 23 | // eslint-disable-next-line no-console 24 | console.warn(err); 25 | } else { 26 | // throw error 27 | throw err; 28 | } 29 | }); 30 | 31 | archive.on('error', (err) => { 32 | throw err; 33 | }); 34 | 35 | archive.pipe(output); 36 | 37 | archive.directory('build/', 'build'); 38 | archive.directory('server/', 'server'); 39 | archive.file('package-lock.json', { name: 'package-lock.json' }); 40 | archive.file('package.json', { name: 'package.json' }); 41 | archive.file('ecosystem.config.js', { name: 'ecosystem.config.js' }); 42 | archive.file('utils/wget.js', { name: 'utils/wget.js' }); 43 | 44 | archive.finalize(); -------------------------------------------------------------------------------- /utils/wget.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const urlParam = process.argv[2]; 3 | const urlParts = new URL(decodeURIComponent(urlParam)).pathname.split('/'); 4 | const [fileName] = urlParts.slice(-1); 5 | execSync(`wget -O ${fileName} ${urlParam}`); --------------------------------------------------------------------------------