From 22cdee72ff0ae0dbed7fb67d862dc6cab6e57ff0 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Sun, 28 Apr 2024 20:44:30 -0400 Subject: [PATCH] Add a JS copy button to the HTML --- .prettierignore | 1 + build.rs | 45 +++++++++++++------ package.json | 4 +- pnpm-lock.yaml | 30 ++++++++----- src/html.rs | 34 +++++++++++--- tailwind.config.js | 2 +- tsconfig.json | 109 +++++++++++++++++++++++++++++++++++++++++++++ ui/index.ts | 35 +++++++++++++++ ui/ui.html | 1 + 9 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 tsconfig.json create mode 100644 ui/index.ts diff --git a/.prettierignore b/.prettierignore index 88820c6..79196cc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ conformance pnpm-lock.yaml +tsconfig.json diff --git a/build.rs b/build.rs index 8ddb2d9..094730f 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ use std::{ env, fs::{self, File}, io::Write, - path::PathBuf, + path::{Path, PathBuf}, }; fn main() { @@ -18,19 +18,12 @@ fn main() { let entry = fs::read_to_string(&asset_index).expect("Could not read the UI entrypoint"); - let find_css = - Regex::new(r#"href="/assets/(ui.[a-zA-Z0-9]+.css)""#).expect("Invalid CSS regex"); - let (_, [css_name]) = find_css - .captures(&entry) - .expect("Could not find CSS") - .extract(); - - let css = asset_root.join(css_name); - let css_map = { - let mut c = css.clone(); - c.as_mut_os_string().push(".map"); - c - }; + let (css_name, css, css_map) = extract_asset(&entry, &asset_root, { + r#"href="/assets/(ui.[a-zA-Z0-9]+.css)""# + }); + let (js_name, js, js_map) = extract_asset(&entry, &asset_root, { + r#"src="/assets/(ui.[a-zA-Z0-9]+.js)""# + }); let out_path = env::var("OUT_DIR").expect("`OUT_DIR` must be set"); let mut out_path = PathBuf::from(out_path); @@ -59,11 +52,18 @@ fn main() { pub const CSS_NAME: &str = "{css_name}"; pub const CSS: &str = include_str!("{css}"); pub const CSS_MAP: &str = include_str!("{css_map}"); + + pub const JS_NAME: &str = "{js_name}"; + pub const JS: &str = include_str!("{js}"); + pub const JS_MAP: &str = include_str!("{js_map}"); "##, asset_index = asset_index.display(), css_name = css_name.escape_default(), css = css.display(), css_map = css_map.display(), + js_name = js_name.escape_default(), + js = js.display(), + js_map = js_map.display(), ) .expect("Could not write HTML assets file"); @@ -73,3 +73,20 @@ fn main() { asset_index = asset_index.display(), ); } + +fn extract_asset<'a>(entry: &'a str, asset_root: &Path, re: &str) -> (&'a str, PathBuf, PathBuf) { + let find_asset = Regex::new(re).expect("Invalid asset regex"); + let (_, [asset_name]) = find_asset + .captures(&entry) + .expect("Could not find asset") + .extract(); + + let asset = asset_root.join(asset_name); + let asset_map = { + let mut a = asset.clone(); + a.as_mut_os_string().push(".map"); + a + }; + + (asset_name, asset, asset_map) +} diff --git a/package.json b/package.json index 455dfb4..fc802c5 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "parcel": "^2.12.0", "postcss": "^8.4.38", "prettier": "^3.2.5", - "tailwindcss": "^3.4.3" + "tailwindcss": "^3.4.3", + "typescript": "^5.4.5" }, "scripts": { "watch": "parcel watch", "build": "parcel build", + "check": "tsc --noEmit", "fmt": "prettier . --write", "fmt:check": "prettier . --check" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 132bac7..e4e36b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: devDependencies: parcel: specifier: ^2.12.0 - version: 2.12.0(postcss@8.4.38) + version: 2.12.0(postcss@8.4.38)(typescript@5.4.5) postcss: specifier: ^8.4.38 version: 8.4.38 @@ -17,6 +17,9 @@ devDependencies: tailwindcss: specifier: ^3.4.3 version: 3.4.3 + typescript: + specifier: ^5.4.5 + version: 5.4.5 packages: @@ -273,7 +276,7 @@ packages: - '@swc/helpers' dev: true - /@parcel/config-default@2.12.0(@parcel/core@2.12.0)(postcss@8.4.38): + /@parcel/config-default@2.12.0(@parcel/core@2.12.0)(postcss@8.4.38)(typescript@5.4.5): resolution: {integrity: sha512-dPNe2n9eEsKRc1soWIY0yToMUPirPIa2QhxcCB3Z5RjpDGIXm0pds+BaiqY6uGLEEzsjhRO0ujd4v2Rmm0vuFg==} peerDependencies: '@parcel/core': ^2.12.0 @@ -283,7 +286,7 @@ packages: '@parcel/core': 2.12.0 '@parcel/namer-default': 2.12.0(@parcel/core@2.12.0) '@parcel/optimizer-css': 2.12.0(@parcel/core@2.12.0) - '@parcel/optimizer-htmlnano': 2.12.0(@parcel/core@2.12.0)(postcss@8.4.38) + '@parcel/optimizer-htmlnano': 2.12.0(@parcel/core@2.12.0)(postcss@8.4.38)(typescript@5.4.5) '@parcel/optimizer-image': 2.12.0(@parcel/core@2.12.0) '@parcel/optimizer-svgo': 2.12.0(@parcel/core@2.12.0) '@parcel/optimizer-swc': 2.12.0(@parcel/core@2.12.0) @@ -449,12 +452,12 @@ packages: - '@swc/helpers' dev: true - /@parcel/optimizer-htmlnano@2.12.0(@parcel/core@2.12.0)(postcss@8.4.38): + /@parcel/optimizer-htmlnano@2.12.0(@parcel/core@2.12.0)(postcss@8.4.38)(typescript@5.4.5): resolution: {integrity: sha512-MfPMeCrT8FYiOrpFHVR+NcZQlXAptK2r4nGJjfT+ndPBhEEZp4yyL7n1y7HfX9geg5altc4WTb4Gug7rCoW8VQ==} engines: {node: '>= 12.0.0', parcel: ^2.12.0} dependencies: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) - htmlnano: 2.1.0(postcss@8.4.38)(svgo@2.8.0) + htmlnano: 2.1.0(postcss@8.4.38)(svgo@2.8.0)(typescript@5.4.5) nullthrows: 1.1.1 posthtml: 0.16.6 svgo: 2.8.0 @@ -1413,7 +1416,7 @@ packages: engines: {node: '>= 10'} dev: true - /cosmiconfig@8.3.6: + /cosmiconfig@8.3.6(typescript@5.4.5): resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} peerDependencies: @@ -1426,6 +1429,7 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + typescript: 5.4.5 dev: true /cross-spawn@7.0.3: @@ -1668,7 +1672,7 @@ packages: function-bind: 1.1.2 dev: true - /htmlnano@2.1.0(postcss@8.4.38)(svgo@2.8.0): + /htmlnano@2.1.0(postcss@8.4.38)(svgo@2.8.0)(typescript@5.4.5): resolution: {integrity: sha512-jVGRE0Ep9byMBKEu0Vxgl8dhXYOUk0iNQ2pjsG+BcRB0u0oDF5A9p/iBGMg/PGKYUyMD0OAGu8dVT5Lzj8S58g==} peerDependencies: cssnano: ^6.0.0 @@ -1697,7 +1701,7 @@ packages: uncss: optional: true dependencies: - cosmiconfig: 8.3.6 + cosmiconfig: 8.3.6(typescript@5.4.5) postcss: 8.4.38 posthtml: 0.16.6 svgo: 2.8.0 @@ -2069,12 +2073,12 @@ packages: resolution: {integrity: sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==} dev: true - /parcel@2.12.0(postcss@8.4.38): + /parcel@2.12.0(postcss@8.4.38)(typescript@5.4.5): resolution: {integrity: sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==} engines: {node: '>= 12.0.0'} hasBin: true dependencies: - '@parcel/config-default': 2.12.0(@parcel/core@2.12.0)(postcss@8.4.38) + '@parcel/config-default': 2.12.0(@parcel/core@2.12.0)(postcss@8.4.38)(typescript@5.4.5) '@parcel/core': 2.12.0 '@parcel/diagnostic': 2.12.0 '@parcel/events': 2.12.0 @@ -2519,6 +2523,12 @@ packages: engines: {node: '>=10'} dev: true + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /update-browserslist-db@1.0.13(browserslist@4.23.0): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true diff --git a/src/html.rs b/src/html.rs index b33bdc5..13ee33a 100644 --- a/src/html.rs +++ b/src/html.rs @@ -20,11 +20,7 @@ pub fn write(registry: &Registry) -> Result<(), Error> { let assets_dir = registry.path.join("assets"); fs::create_dir_all(&assets_dir).context(AssetDirSnafu { path: &assets_dir })?; - let css_path = { - let mut css_path = assets_dir; - css_path.push(assets::CSS_NAME); - css_path - }; + let css_path = assets_dir.join(assets::CSS_NAME); fs::write(&css_path, assets::CSS).context(CssSnafu { path: &css_path })?; let css_map_path = { @@ -36,6 +32,16 @@ pub fn write(registry: &Registry) -> Result<(), Error> { path: &css_map_path, })?; + let js_path = assets_dir.join(assets::JS_NAME); + fs::write(&js_path, assets::JS).context(JsSnafu { path: &js_path })?; + + let js_map_path = { + let mut js_map_path = js_path; + js_map_path.as_mut_os_string().push(".map"); + js_map_path + }; + fs::write(&js_map_path, assets::JS_MAP).context(JsMapSnafu { path: &js_map_path })?; + Ok(()) } @@ -53,6 +59,12 @@ pub enum Error { #[snafu(display("Could not write the CSS sourcemap file to {}", path.display()))] CssMap { source: io::Error, path: PathBuf }, + + #[snafu(display("Could not write the JS file to {}", path.display()))] + Js { source: io::Error, path: PathBuf }, + + #[snafu(display("Could not write the JS sourcemap file to {}", path.display()))] + JsMap { source: io::Error, path: PathBuf }, } const CARGO_DOCS: &str = @@ -93,9 +105,17 @@ fn index(config: &ConfigV1, crates: &ListAll) -> Markup { fn code_block(content: impl AsRef) -> Markup { let content = content.as_ref(); + let span_class = "col-start-1 row-start-1 leading-none p-1"; + html! { - pre class="border border-black bg-theme-rose-light m-1 p-1 overflow-x-auto" { - code { (content) } + mg-copy { + pre class="relative border border-black bg-theme-rose-light m-1 p-1 overflow-x-auto" { + button class="hidden absolute top-0 right-0 grid" data-target="copy" { + span class=(span_class) data-target="state0" { "Copy" } + span class={(span_class) " invisible"} data-target="state1" { "Copied" } + } + code data-target="content" { (content) } + } } } } diff --git a/tailwind.config.js b/tailwind.config.js index 7e3d700..daa335f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/html.rs"], + content: ["./src/html.rs", "./ui/*.{html,ts}"], theme: { extend: { colors: { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f7b9163 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/ui/index.ts b/ui/index.ts new file mode 100644 index 0000000..17a3cfa --- /dev/null +++ b/ui/index.ts @@ -0,0 +1,35 @@ +class Copy extends HTMLElement { + connectedCallback() { + let button = this.querySelector('[data-target = "copy"]'); + let state0 = this.querySelector('[data-target = "state0"]'); + let state1 = this.querySelector('[data-target = "state1"]'); + + if (!(button && state0 && state1)) { + return; + } + + const swapState = () => { + state0.classList.toggle("invisible"); + state1.classList.toggle("invisible"); + }; + + button.addEventListener("click", (evt) => { + evt.preventDefault(); + + let content = this.querySelector('[data-target = "content"]'); + let text = content?.textContent; + if (!text) { + return; + } + + navigator.clipboard.writeText(text); + + swapState(); + window.setTimeout(swapState, 1000); + }); + + button.classList.remove("hidden"); + } +} + +window.customElements.define("mg-copy", Copy); diff --git a/ui/ui.html b/ui/ui.html index 6857cf3..ca55c2b 100644 --- a/ui/ui.html +++ b/ui/ui.html @@ -1 +1,2 @@ +