default: @just --list # Clean build artifacts (but keep dependencies) alias clean := soft-clean soft-clean: #!/usr/bin/env bash set -eo pipefail # Clean both the root workspace and standalone Desktop build artifacts. cargo clean (cd "{{justfile_directory()}}/harper-desktop/src-tauri" && cargo clean) # Handle packages/* # The path stem is not combined into one file expansion because if they pop up into # another directory, there is a chance they should not be removed. rm -rf "{{justfile_directory()}}"/packages/chrome-plugin/{build,package} rm -rf "{{justfile_directory()}}"/packages/components/{.svelte-kit,dist} rm -rf "{{justfile_directory()}}"/packages/harper.js/{dist,markdown,temp} rm -rf "{{justfile_directory()}}"/packages/lint-framework/{dist} rm -rf "{{justfile_directory()}}"/packages/obsidian-plugin/{harper-obsidian-plugin.zip,main.js} rm -rf "{{justfile_directory()}}"/packages/vscode-plugin/{.vscode-test,bin,build} rm -rf "{{justfile_directory()}}"/packages/web/{.svelte-kit,build} rm -rf "{{justfile_directory()}}"/packages/wordpress-plugin/{build,harper.zip} rm -rf "{{justfile_directory()}}"/harper-desktop/{.svelte-kit,build,package} rm -rf "{{justfile_directory()}}"/harper-wasm/pkg # Hard clean all build artifacts and dependencies hard-clean: soft-clean #!/usr/bin/env bash set -eo pipefail # Remove all node dependencies find "{{justfile_directory()}}" -type d -name "node_modules" -prune -exec rm -rf {} + # Prune node cache pnpm store prune # Format entire project alias fmt := format format: cargo fmt cd "{{justfile_directory()}}/harper-desktop/src-tauri" && cargo fmt pnpm format # Build the shared component library build-components: #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/components" pnpm install --engine-strict=false pnpm build # Build the shared Harper editor library build-harper-editor: build-lint-framework build-components #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/harper-editor" pnpm install --engine-strict=false pnpm build # Build the WebAssembly module build-wasm: #!/usr/bin/env bash export CARGO_TERM_QUIET=true cd "{{justfile_directory()}}/harper-wasm" if [ "${DISABLE_WASM_OPT:-0}" -eq 1 ]; then wasm-pack build --target web --no-opt --out-name harper_wasm wasm-pack build --target web --no-opt --out-name harper_wasm_slim --no-default-features else wasm-pack build --target web --out-name harper_wasm wasm-pack build --target web --out-name harper_wasm_slim --no-default-features fi # Build `harper.js` with all size optimizations available. alias build-harper-js := build-harperjs build-harperjs: build-wasm #!/usr/bin/env bash set -eo pipefail # Removes a duplicate copy of the WASM binary if Vite is left to its devices. perl -pi -e 's/new URL\(.*\)/new URL()/g' "{{justfile_directory()}}/harper-wasm/pkg/harper_wasm.js" perl -pi -e 's/new URL\(.*\)/new URL()/g' "{{justfile_directory()}}/harper-wasm/pkg/harper_wasm_slim.js" cd "{{justfile_directory()}}/packages/harper.js" pnpm install pnpm build # Generate API reference ./docs.sh # Build the browser lint framework module build-lint-framework: build-harperjs #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/lint-framework" pnpm install pnpm build alias test-harper-js := test-harperjs test-harperjs: build-harperjs #!/usr/bin/env bash set -eo pipefail pnpm install cd "{{justfile_directory()}}/packages/harper.js" pnpm playwright install pnpm test # Test runnable examples cd "{{justfile_directory()}}/packages/harper.js/examples/commonjs-simple" pnpm start test-obsidian: build-obsidian #!/usr/bin/env bash set -eo pipefail pnpm install cd "{{justfile_directory()}}/packages/obsidian-plugin" pnpm playwright install pnpm test alias dev-wordpress := dev-wp dev-wp: build-harperjs #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/wordpress-plugin" pnpm install pnpm wp-now start & pnpm start # Build the WordPress plugin alias build-wordpress := build-wp build-wp: build-harperjs #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/wordpress-plugin" pnpm install pnpm build pnpm plugin-zip # Compile the website's dependencies and start a development server. Note that if you make changes to `harper-wasm`, you will have to re-run this command. dev-web: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/web" pnpm install pnpm dev # Build the Harper website. build-web: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/web" pnpm install ENABLE_ADMIN_ROUTES=false pnpm build # Start a development server for Harper Desktop. dev-desktop: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri dev # Start the Harper Desktop highlighter process directly. dev-desktop-highlighter: #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop/src-tauri" cargo run -- highlighter # Check Harper Desktop frontend and Rust targets. check-desktop: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm check just check-desktop-rust # Check formatting and lint all standalone Desktop Rust targets. check-desktop-rust: #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop/src-tauri" cargo fmt -- --check cargo clippy --all-targets -- -Dwarnings -D clippy::dbg_macro -D clippy::needless_raw_string_hashes # Build Harper Desktop Linux bundles. build-desktop-linux: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build -b deb,rpm,appimage # Build Harper Desktop Windows bundles. build-desktop-windows: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail rustup target add x86_64-pc-windows-msvc cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc -b nsis --config '{"bundle":{"createUpdaterArtifacts":false}}' # Build native Windows bundles using a CI-generated Azure signing config. build-desktop-windows-signed signing_config: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build --target x86_64-pc-windows-msvc -b nsis --config {{quote(signing_config)}} # Test desktop signing and release scripts without CI credentials. test-desktop-release: node --test .buildkite/commands/*.test.cjs # Build Harper Desktop for Apple Silicon only — faster than the universal recipe below. build-desktop-macos-arm64: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build -b app,dmg --target aarch64-apple-darwin # Build Harper Desktop macOS bundles. build-desktop-macos: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build -b app,dmg --target universal-apple-darwin # Build Harper Desktop macOS bundles without updater artifacts. build-desktop-macos-unsigned: build-harperjs build-lint-framework build-components build-harper-editor #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-desktop" pnpm install pnpm tauri build -b app,dmg --config '{"bundle":{"createUpdaterArtifacts":false}}' --target universal-apple-darwin # Build the Harper Obsidian plugin. build-obsidian: build-harperjs #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/obsidian-plugin" max_bundle_size_bytes=$((30 * 1024 * 1024)) pnpm install pnpm build bundle_size_bytes=$(wc -c < main.js | tr -d '[:space:]') if [ "$bundle_size_bytes" -gt "$max_bundle_size_bytes" ]; then bundle_size_mb=$(awk "BEGIN { printf \"%.2f\", $bundle_size_bytes / 1024 / 1024 }") max_bundle_size_mb=$(awk "BEGIN { printf \"%.2f\", $max_bundle_size_bytes / 1024 / 1024 }") echo "Obsidian plugin bundle size ${bundle_size_mb} MB exceeds the ${max_bundle_size_mb} MB limit. This can cause problems for mobile devices with limited memory." >&2 exit 1 fi zip harper-obsidian-plugin.zip manifest.json main.js # Build the Chrome extension. alias build-chrome := build-chrome-plugin alias build-chrome-extension := build-chrome-plugin build-chrome-plugin: build-harperjs build-lint-framework build-components #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm install pnpm zip-for-chrome # Start a development server for the Chrome extension. alias dev-chrome := dev-chrome-plugin alias dev-chrome-extension := dev-chrome-plugin dev-chrome-plugin: build-harperjs build-lint-framework build-components #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm install pnpm dev # Build the Firefox extension. alias build-firefox := build-firefox-plugin alias build-firefox-extension := build-firefox-plugin build-firefox-plugin: build-harperjs build-lint-framework build-components #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm install pnpm zip-for-firefox alias test-chrome := test-chrome-plugin alias test-chrome-extension := test-chrome-plugin test-chrome-plugin: build-chrome-plugin #!/usr/bin/env bash set -eo pipefail pnpm install cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm playwright install # For environments without displays like CI servers or containers if [[ "$(uname)" == "Linux" ]]; then env -u WAYLAND_DISPLAY XDG_SESSION_TYPE=x11 \ xvfb-run --auto-servernum pnpm test --project chromium else pnpm test --project chromium fi alias test-firefox := test-firefox-plugin alias test-firefox-extension := test-firefox-plugin test-firefox-plugin: build-firefox-plugin #!/usr/bin/env bash set -eo pipefail pnpm install cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm playwright install # For environments without displays like CI servers or containers if [[ "$(uname)" == "Linux" ]]; then env -u WAYLAND_DISPLAY XDG_SESSION_TYPE=x11 \ xvfb-run --auto-servernum pnpm test --project firefox else pnpm test --project firefox fi # Run VSCode plugin unit and integration tests. alias test-vscode-extension := test-vscode test-vscode: #!/usr/bin/env bash set -eo pipefail # Needed so `pnpm install` can succeed. DISABLE_WASM_OPT=1 just build-harperjs ext_dir="{{justfile_directory()}}/packages/vscode-plugin" bin_dir="${ext_dir}/bin" if ! [[ -d "$bin_dir" ]]; then mkdir "$bin_dir" fi echo Building binaries cargo build --release -p harper-ls cp "{{justfile_directory()}}/target/release/harper-ls"* "$bin_dir" cd "$ext_dir" pnpm install # For environments without displays like CI servers or containers if [[ "$(uname)" == "Linux" ]] && [[ -z "$DISPLAY" ]]; then xvfb-run --auto-servernum pnpm test else pnpm test fi # Over time, VSCode test versions take up space that can be hard to track down if [[ -d .vscode-test ]]; then all_versions=$(ls -1 .vscode-test | grep "^vscode-" | sort -V) latest_version=$(echo "$all_versions" | tail -n 1) old_versions=$(echo "$all_versions" | sed '$d') # Delete last line instead if [[ -n "$old_versions" ]]; then count=$(echo "$old_versions" | wc -l) echo "$old_versions" | xargs -I {} rm -rf .vscode-test/{} echo "✓ Deleted $count old VSCode versions, keeping $latest_version" else echo "✓ No old versions to clean (keeping $latest_version)" fi fi # Build and package the Visual Studio Code extension. # If `target` is passed, it is assumed that `harper-ls` has been compiled beforehand and is in `packages/vscode-plugin/bin`. This is used in CI. alias package-vscode-extension := package-vscode package-vscode target="": #!/usr/bin/env bash set -eo pipefail ext_dir="{{justfile_directory()}}/packages/vscode-plugin" bin_dir="${ext_dir}/bin" cp LICENSE "$ext_dir" if [[ -z "{{target}}" ]]; then echo Building binaries cargo build --release -q if ! [[ -d "$bin_dir" ]]; then mkdir "$bin_dir" fi cp "{{justfile_directory()}}/target/release/harper-ls"* "$bin_dir" fi cd "$ext_dir" pnpm install if [[ -n "{{target}}" ]]; then pnpm package --target {{target}} else pnpm package fi update-vscode-linters: #!/usr/bin/env bash set -eo pipefail linters_file="$(mktemp)" output_file="$(mktemp)" trap 'rm -f "$linters_file" "$output_file"' EXIT cargo run --bin harper-cli -- config | jq 'with_entries(.key |= "harper.linters." + . | .value |= { "scope": "resource", "type": "boolean", "default": .default_value, "description": .description } )' > "$linters_file" cd "{{justfile_directory()}}/packages/vscode-plugin" jq 'walk( if type == "object" then with_entries(select(.key | startswith("harper.linters") | not)) end )' package.json | jq --slurpfile linters "$linters_file" \ '.contributes.configuration.properties += $linters[0]' > \ "$output_file" mv "$output_file" package.json just format # Run Rust formatting and linting. check-rust: audit-dictionary #!/usr/bin/env bash set -eo pipefail cargo fmt -- --check cargo clippy -- -Dwarnings -D clippy::dbg_macro -D clippy::needless_raw_string_hashes cargo hack check --each-feature just check-desktop-rust # Perform format and type checking. check: check-rust check-js check-js: build-harperjs build-lint-framework build-components build-harper-editor build-web #!/usr/bin/env bash set -eo pipefail pnpm install pnpm check cd "{{justfile_directory()}}/packages/chrome-plugin" pnpm check cd "{{justfile_directory()}}/packages/obsidian-plugin" pnpm check cd "{{justfile_directory()}}/packages/wordpress-plugin" pnpm check cd "{{justfile_directory()}}/packages/components" pnpm check cd "{{justfile_directory()}}/packages/harper-editor" pnpm check cd "{{justfile_directory()}}/packages/web" ENABLE_ADMIN_ROUTES=false pnpm check # Populate build caches and install necessary local tooling (tools callable via `pnpm run `). setup: build-harperjs test-harperjs test-vscode build-web build-wp build-obsidian build-chrome-plugin # Perform full format and type checking, build all projects and run all tests. Run this before pushing your code. precommit: check test build-harperjs build-obsidian build-web build-wp build-firefox-plugin build-chrome-plugin #!/usr/bin/env bash set -eo pipefail echo Building binaries cargo build --all-targets -q # Install `harper-cli` and `harper-ls` to your machine via `cargo` install: cargo install --path harper-ls --locked cargo install --path harper-cli --locked # Run `harper-cli` on the Harper repository dogfood: #!/usr/bin/env bash cargo build --release if command -v fd &> /dev/null; then # Use fd if available (faster and more user-friendly) fd_cmd() { fd -e rs; } else # Fall back to find if fd is not installed fd_cmd() { find . -name "*.rs" -type f; } fi fd_cmd | while read -r file; do echo "Linting $file" ./target/release/harper-cli lint "$file" done test-rust: echo Running all Rust tests cargo test -q cd "{{justfile_directory()}}/harper-desktop/src-tauri" && cargo test -q # Test everything. test: test-rust test-harperjs test-vscode test-obsidian test-chrome-plugin test-firefox-plugin # Use `harper-cli` to parse a provided file and print out the resulting tokens. parse file: cargo run --bin harper-cli -- parse {{file}} # Lint provided inputs using Harper and print the results. # The inputs can be files, directories, or a string on the command line. # If no inputs are provided, lint stdin. lint *inputs: cargo run --bin harper-cli -- lint {{inputs}} # Show the spans of the parsed tokens overlapped in the provided file. spans file: cargo run --bin harper-cli -- spans {{file}} # Add a noun to Harper's curated dictionary. alias add-noun := addnoun addnoun noun: #!/usr/bin/env bash DICT_FILE=./harper-core/dictionary.dict cat $DICT_FILE | grep "^{{noun}}/" if [ $? -eq 0 ] then echo "That noun may already be in the dictionary." exit 0 fi # 'g': possessive -'s suffix for both common and proper nouns flags='g' # If the first letter is uppercase, treat it as a proper noun if [[ "{{noun}}" =~ ^[A-Z] ]]; then # 'O': proper noun, usually no plural flags+='O' else # 'N': (common) singular noun, 'S': plural -(e)s flags+='NS' fi # Echo the noun with its flags to the dictionary file [[ -s $DICT_FILE && -n $(tail -c1 "$DICT_FILE") ]] && echo >> "$DICT_FILE" echo "{{noun}}/$flags" >> "$DICT_FILE" # Search Harper's curated dictionary for a specific word search-dict-for word: #!/usr/bin/env bash if command -v rg > /dev/null; then cargo run --bin harper-cli -- words | rg {{word}} else cargo run --bin harper-cli -- words | grep {{word}} fi # Find words in the user's `harper-ls/dictionary.txt` for words already in the curated dictionary. user-dict-overlap: #!/usr/bin/env bash USER_DICT_FILE="$HOME/.config/harper-ls/dictionary.txt" while read -r line; do just searchdictfor $line 2> /dev/null done < $USER_DICT_FILE # Get the metadata associated with one or more words in Harper's dictionary as JSON. get-metadata *words: cargo run --bin harper-cli -- metadata {{words}} get-metadata-brief *words: cargo run --bin harper-cli -- metadata --brief {{words}} # Get all the forms of a word using the affixes. get-forms +words: cargo run --bin harper-cli -- forms {{words}} # Get a random sample of words from Harper's dictionary and list all forms of each. sample-forms count: #!/usr/bin/env bash set -eo pipefail DICT_FILE=./harper-core/dictionary.dict # USER_DICT_FILE="$HOME/.config/harper-ls/dictionary.txt" if [ "{{count}}" -eq 0 ]; then exit 0 fi total_lines=$(wc -l < $DICT_FILE) # Cross-platform random line selection if command -v shuf >/dev/null 2>&1; then words=$(shuf -n "{{count}}" "$DICT_FILE") elif command -v jot >/dev/null 2>&1; then words=$(jot -r "{{count}}" 1 "$total_lines" | while read -r line_num; do \ sed -n "$line_num"p "$DICT_FILE"; \ done) else echo "Error: Neither 'shuf' nor 'jot' found. Cannot generate random words." >&2 exit 1 fi cargo run --bin harper-cli -- forms $words bump-versions: update-vscode-linters #!/usr/bin/env bash set -eo pipefail # Include private workspace crates; standalone Desktop is updated below. cargo ws version --all --no-git-push --no-git-tag --force '*' HARPER_VERSION=$(tq --raw --file harper-core/Cargo.toml .package.version) cd "{{justfile_directory()}}/packages/harper.js" cat package.json | jq ".version = \"$HARPER_VERSION\"" > package.json.edited mv package.json.edited package.json cd "{{justfile_directory()}}/packages/vscode-plugin" cat package.json | jq ".version = \"$HARPER_VERSION\"" > package.json.edited mv package.json.edited package.json cd "{{justfile_directory()}}/packages/chrome-plugin" cat package.json | jq ".version = \"$HARPER_VERSION\"" > package.json.edited mv package.json.edited package.json cd "{{justfile_directory()}}/packages/obsidian-plugin" cat package.json | jq ".version = \"$HARPER_VERSION\"" > package.json.edited mv package.json.edited package.json cd "{{justfile_directory()}}/harper-desktop" cat package.json | jq ".version = \"$HARPER_VERSION\"" > package.json.edited mv package.json.edited package.json cd "{{justfile_directory()}}/harper-desktop/src-tauri" # Desktop is outside the workspace, so synchronize its crate and lockfile explicitly. HARPER_VERSION="$HARPER_VERSION" perl -pi -e 's/^version = "[^"]+"$/version = "$ENV{HARPER_VERSION}"/' Cargo.toml cargo update --workspace cat tauri.conf.json | jq ".version = \"$HARPER_VERSION\"" > tauri.conf.json.edited mv tauri.conf.json.edited tauri.conf.json just format lazygit # Enter an infinite loop of property testing until a bug is found. fuzz: #!/usr/bin/env bash while true do QUICKCHECK_TESTS=100000 cargo test if [[ x$? != x0 ]] ; then exit $? fi done register-linter module name: #!/usr/bin/env bash D="{{justfile_directory()}}/harper-core/src/linting" sed -i "/pub use an_a::AnA;/a pub use {{module}}::{{name}};" "$D/mod.rs" sed -i "/use super::an_a::AnA;/a use super::{{module}}::{{name}};" "$D/lint_group.rs" sed -i "/insert_expr_rule!(ChockFull, true);/a \ \ \ \ insert_struct_rule!({{name}}, true);" "$D/lint_group.rs" just format # Print annotations and their descriptions from annotations.json alias print-affixes := print-annotations alias get-annotations := print-annotations alias list-annotations := print-annotations alias show-annotations := print-annotations print-annotations: #! /usr/bin/env node const affixesData = require('{{justfile_directory()}}/harper-core/annotations.json'); const allEntries = { ...affixesData.affixes || {}, ...affixesData.properties || {} }; // Calculate the maximum description length for alignment const entries = Object.entries(allEntries); const maxDescLength = entries.reduce((max, [flag, fields]) => { const description = fields['#'] || ''; const lineLength = flag.length + 2 + description.length; // flag + ': ' + description return Math.max(max, lineLength); }, 0); entries.sort((a, b) => a[0].localeCompare(b[0])).forEach(([flag, fields]) => { const description = fields['#'] || ''; const comment = fields['//'] || null; if (description) { const line = `${flag}: ${description}`; const padding = ' '.repeat(Math.max(1, maxDescLength - line.length + 2)); console.log(line + (comment ? `${padding}// ${comment}` : '')); } }); console.log('Available letters for new flags:', [...Array.from({length: 26}, (_, i) => [String.fromCharCode(65 + i), String.fromCharCode(97 + i)] ).flat()].filter(letter => !Object.keys(allEntries).includes(letter)).sort().join(' ')); console.log('Available digits for new flags:', [...Array.from({length: 10}, (_, i) => String(i) )].filter(digit => !Object.keys(allEntries).includes(digit)).sort().join(' ')); console.log('Available symbols for new flags:', [...Array.from('!"#$%&\'()*+,-./:;<=>?@\[\\\]\^_`{|}~')] .filter(symbol => !Object.keys(allEntries).includes(symbol)).sort().join(' ')); console.log('Available Latin-1 characters for new flags:'); [...Array.from({length: 256-160}, (_, i) => String.fromCharCode(160 + i))] .filter(char => !Object.keys(allEntries).includes(char) && char.charCodeAt(0) !== 160 && char.charCodeAt(0) !== 173) .sort() .join(' ') .match(/.{1,64}/g) .forEach(line => console.log(' ' + line)); # Get the most recent changes to the curated dictionary. Includes an optional argument to specify the number of commits to look back. Defaults to 1. newest-dict-changes *numCommits: #! /usr/bin/env node const { exec } = require('child_process'); const DICT_FILE = 'harper-core/dictionary.dict'; const [RST, BOLD, DIM, ITAL, NORM] = [0, 1, 2, 3, 22].map(c => `\x1b[${c}m`); const [RED, GRN, YLW, BLU, MGN, CYN, WHT] = [1, 2, 3, 4, 5, 6, 7].map(c => `\x1b[${30+c}m`); const argv = [...process.argv]; const [showHashes, showDiff] = ["--show-hashes", "--show-diff"].map(flag => argv.includes(flag) && argv.splice(argv.indexOf(flag), 1)); // uncomment first line to use in justfile, comment out second line to use standalone const numCommits = "{{numCommits}}" || 1; // const numCommits = argv[2] || 1; // Command to get the last commit hash that modified the specified file const hashCommand = `git log --no-merges -n ${numCommits} --format="%H" -- ${DICT_FILE}`; console.log(`${MGN}${BOLD}GET HASHES${NORM}: ${hashCommand}${RST}`); // Execute the command to get the hash exec(hashCommand, (error, hashString, stderr) => { if (error) return console.error(`Error executing command: ${error.message}`); if (stderr) return console.error(`stderr: ${stderr}`); // avoid empty last line const longHashes = hashString.trim().split('\n'); if (showHashes) console.log(longHashes.length, longHashes); if (longHashes.length < 1) { console.error('No hash(es) returned. Exiting.'); process.exit(1); } // keep the last line and second last if there's more than one hash const [hash2, hash1] = longHashes.slice(-2).map((h) => h.substring(0, 7)); // Command to get the word-level diff using the retrieved hash, using either one or two hashes const hashes = longHashes.length == 1 ? `${hash2}` : `${hash1} ${hash2}`; const diffCommand = `git diff --word-diff --no-color --unified=0 ${hashes} -- ${DICT_FILE}`; console.log(`${MGN}${BOLD}GET DIFF${NORM}: ${diffCommand}${RST}`); // Execute the diff command with a large buffer to avoid failing to handle large diffs such as: // git diff --word-diff --no-color --unified=0 0761702 baeb08e -- harper-core/dictionary.dict exec(diffCommand, { maxBuffer: 2048 * 1024 }, (diffError, diffString, diffStderr) => { if (diffError) { console.error(`Error executing diff command: ${diffError.message}`); return; } if (diffStderr) return console.error(`stderr: ${diffStderr}`); if (showDiff) console.log(`DIFFSTART\n${diffString}\nDIFFEND`); // uncomment first line to use in justfile, comment out second line to use standalone const affixes = require('{{justfile_directory()}}/harper-core/annotations.json').affixes; // const affixes = require('./harper-core/annotations.json').affixes; diffString.split("\n").forEach(line => { const match = line.match(/^(?:\[-(.*?)-\])?(?:\{\+(.*?)\+\})?$/); if (match) { let [, before, after] = match; if (before && after) { // An entry changed const [[oldword, oldaff], [newword, newaff]] = [before, after].map(e => e.split('/')); if (oldword === newword) { if (oldaff !== newaff) { const [oldRest, newRest] = [oldaff, newaff].map(aff => aff ? `${DIM}/${aff}${RST}`: ''); console.log(`${BOLD}${CYN}CHG${RST} # ${oldword}${oldRest} -> ${newRest}`); const [oldNorm, newNorm] = [oldaff, newaff].map(a => a ? a.split(''): []) .map(a => new Set(a)) .map(a => Array.from(a)) .map(a => a.sort()); const removed = oldNorm.filter(o => !newNorm.includes(o)); const added = newNorm.filter(n => !oldNorm.includes(n)); const [addStr, remStr] = [added, removed] .map(a => a.map(a => ` ${BOLD}${ITAL}${a}${RST} -> ${ (affixes[a] && affixes[a]['#']) || '???' }`) .join('\n') ); if (removed.length > 0) console.log(`${RED} ${BOLD}REMOVED${RST}:\n${remStr}`); if (added.length > 0) console.log(`${GRN} ${BOLD}ADDED${RST}:\n${addStr}`); } else { // should never happen console.log(`${YLW} ?NO AFFIX CHG? '${oldaff}' -> '${newaff}'${RST}`); } } else { // The word changed rather than its affixes console.log(`${YLW} ${BOLD}CHANGED${RST} ${RED}${oldword}${RST} -> ${GRN}${newword}${RST}`); } } else if (before || after) { // An entry was added or removed const [entry, symbol, action, colour] = before ? [before, "-", 'DEL', RED] : [after, "+", 'ADD', GRN]; const [word, affix] = entry.split('/'); console.log(`${colour}${BOLD}${action}${RST} ${symbol} ${word}${ affix ? `${DIM}/${affix}` : '' }${RST}`); } } }); }); }); # Print the input string or file with nominal phrases highlighted. These are generated using Harper's chunker. alias get-nominal-phrases := get-nps alias get-noun-phrases := get-nps get-nps text: cargo run --bin harper-cli -- nominal-phrases "{{text}}" # Suggest annotations for a potential new property annotation suggest-annotation input: #! /usr/bin/env node const affixesData = require('{{justfile_directory()}}/harper-core/annotations.json'); const allEntries = { ...affixesData.affixes || {}, ...affixesData.properties || {} }; // Get all used flags const usedFlags = new Set(Object.keys(allEntries)); // Process input string and check both cases const input = '{{input}}'; const normalizedInput = input.replace(/\s/g, ''); const uniqueChars = [...new Set(normalizedInput.toUpperCase() + normalizedInput.toLowerCase())]; console.log(`Checking input: "${input}"\n${'='.repeat(50)}`); // Check each character in input const availableChars = [...new Set(uniqueChars)] .filter(char => !usedFlags.has(char)); if (availableChars.length > 0) { console.log(`These characters of "${input}" are available to use for new annotations:`); availableChars.forEach(char => console.log(` '${char}' (${char.charCodeAt(0)})`)); } else { const inputChars = new Set(normalizedInput.toLowerCase() + normalizedInput.toUpperCase()); const renamable = Object.entries(allEntries) .filter(([flag, entry]) => entry.rename_ok && inputChars.has(flag)) .sort((a, b) => a[0].localeCompare(b[0])); if (renamable.length > 0) { console.log(`None of the characters of "${input}" are available to use for new annotations, but these ones are OK to be moved to make way for new annotations:`); renamable.forEach(([flag, entry]) => { console.log(` '${flag}': ${entry['#'] || 'No description'}${entry['//'] ? ` (${entry['//']})` : ''}`); }); } else { console.log(`None of the characters of "${input}" are available to use for new annotations, and none of them are OK to be moved to make way for new annotations.`); } } # Audit the curated dictionary for any issues. alias audit-dict := audit-dictionary audit-dictionary DIR="harper-core": cargo run --bin harper-cli -- audit-dictionary {{DIR}} alias test-snapshots := run-snapshots alias test-pos-tagger := run-snapshots alias test-pos-tags := run-snapshots alias test-pos-tagging := run-snapshots run-snapshots: #!/usr/bin/env bash set -eo pipefail cd harper-core cargo test -- test_pos_tagger test_most_lints # list configuration groups by label and description, with settings if mode is verbose ls-config mode="brief": #! /usr/bin/env node const verbose = '{{mode}}' === 'verbose'; const config = JSON.parse(require('fs').readFileSync(require('path').join('{{justfile_directory()}}', 'harper-core/default_config.json'), 'utf8')).settings; const formatLine = (items, maxLen = 120) => { const lines = []; let line = ' '; items.forEach((item, i) => { const comma = i < items.length - 1 ? ', ' : ''; const wouldExceed = line.length + item.length + comma.length > maxLen; if (wouldExceed && line !== ' ') { lines.push(line); line = ' '; } line += item + comma; }); lines.push(line); return lines.join('\n'); }; config.forEach(g => { console.log(`\x1b[1m${g.Group.label}\x1b[0m: \x1b[36m${g.Group.description}\x1b[0m`); if (verbose) { const names = g.Group.child.settings.map(s => s.Bool.name); console.log(formatLine(names)); } }); # Search configuration groups for substring in label or description grep-config query: #! /usr/bin/env node const q = '{{query}}'.toLowerCase(); const config = JSON.parse(require('fs').readFileSync(require('path').join('{{justfile_directory()}}', 'harper-core/default_config.json'), 'utf8')).settings; config.filter(g => g.Group.label.toLowerCase().includes(q) || g.Group.description.toLowerCase().includes(q)) .forEach(g => console.log(`\x1b[1m${g.Group.label}\x1b[0m: \x1b[36m${g.Group.description}\x1b[0m`)); # Run the native allocation profiler for spell-check operations. alias alloc-prof := alloc-profile alloc-profile: cargo run --example alloc_profile -p harper-core --release # Run native benchmarks. bench: cargo bench # Build harper-wasm with bench support and run the WASM benchmark harness. # Runs wasm-opt by default to match the shipping build; set DISABLE_WASM_OPT=1 # to skip it (faster rebuilds, non-shipping numbers). bench-wasm: #!/usr/bin/env bash set -eo pipefail cd "{{justfile_directory()}}/harper-wasm" if [ "${DISABLE_WASM_OPT:-0}" -eq 1 ]; then wasm-pack build --target web --no-opt --out-dir pkg-bench --features bench else wasm-pack build --target web --out-dir pkg-bench --features bench fi node benches/wasm_bench.js # search configuration group settings for substring in name grep-config-settings query: #! /usr/bin/env node const q = '{{query}}'.toLowerCase(); const config = JSON.parse(require('fs').readFileSync(require('path').join('{{justfile_directory()}}', 'harper-core/default_config.json'), 'utf8')).settings; const formatLine = (items, maxLen = 120) => { const lines = []; let line = ' '; items.forEach((item, i) => { const comma = i < items.length - 1 ? ', ' : ''; const wouldExceed = line.length + item.length + comma.length > maxLen; if (wouldExceed && line !== ' ') { lines.push(line); line = ' '; } line += item + comma; }); lines.push(line); return lines.join('\n'); }; config.forEach(g => { const matches = g.Group.child.settings .filter(s => s.Bool.name.toLowerCase().includes(q)) .map(s => s.Bool.name); if (matches.length) { console.log(`\x1b[1m${g.Group.label}\x1b[0m:`); console.log(`\x1b[36m${formatLine(matches)}\x1b[0m`); } }); # Sort nested child settings in default_config.json by name sort-config-settings: #! /usr/bin/env node const fs = require('fs'); const path = require('path'); const configPath = path.join('{{justfile_directory()}}', 'harper-core/default_config.json'); const inputJson = JSON.parse(fs.readFileSync(configPath, 'utf8')); // Sort the nested child settings inputJson.settings.forEach(item => { if (item.Group && item.Group.child && item.Group.child.settings) { item.Group.child.settings.sort((a, b) => { // Extract the name property from the inner object (e.g., 'Bool') const nameA = Object.values(a)[0].name; const nameB = Object.values(b)[0].name; return nameA.localeCompare(nameB); }); } }); fs.writeFileSync(configPath, JSON.stringify(inputJson, null, 2) + '\n'); console.log('Sorted default_config.json child settings by name.');