mirror of
https://github.com/sstallion/obsidian-command-line.git
synced 2026-07-22 05:42:07 +00:00
Initial commit
This commit is contained in:
commit
d629cb3633
28 changed files with 5992 additions and 0 deletions
21
.editorconfig
Normal file
21
.editorconfig
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 80
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{mjs,ts}]
|
||||
max_line_length = 85
|
||||
|
||||
[*.{json,yml}]
|
||||
indent_size = 2
|
||||
22
.github/workflows/ci.yml
vendored
Normal file
22
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- name: Run checks
|
||||
run: npm run check
|
||||
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build plugin
|
||||
run: npm run release
|
||||
- id: extract-release-notes
|
||||
uses: ffurrer2/extract-release-notes@v2
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: ${{ steps.extract-release-notes.outputs.release_notes }}
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/node_modules/
|
||||
/data.json
|
||||
/main.js
|
||||
/styles.css
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
git-tag-version=false
|
||||
tag-version-prefix=""
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
*.*
|
||||
!*.json
|
||||
!*.mjs
|
||||
!*.scss
|
||||
!*.ts
|
||||
package-lock.json
|
||||
10
CHANGELOG.md
Normal file
10
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
[Unreleased]: https://github.com/sstallion/obsidian-command-line/compare/TODO...HEAD
|
||||
104
CONTRIBUTING.md
Normal file
104
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Contributing
|
||||
|
||||
If you have an idea or feature request please open an [issue][1], even if you do
|
||||
not have time to contribute!
|
||||
|
||||
## Making Changes
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This guide assumes you have a functioning [Node.js and npm][2] installation. A
|
||||
> recent release ([Node.js 20][3] or later) is required to build this plugin.
|
||||
> Long-term support (LTS) releases are recommended for local development.
|
||||
|
||||
To get started, [fork][4] this repository on GitHub and clone a working copy for
|
||||
development:
|
||||
|
||||
```
|
||||
$ git clone git@github.com:YOUR-USERNAME/obsidian-command-line.git
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> For local development, it is recommended to create the working copy in the
|
||||
> `plugins` directory. The directory name should match the plugin ID. See the
|
||||
> [Obsidian Docs][5] for details.
|
||||
|
||||
After cloning, install dependencies by issuing:
|
||||
|
||||
```
|
||||
$ npm ci
|
||||
```
|
||||
|
||||
Several scripts are defined in [package.json][6] to aid plugin development. Once
|
||||
you have finished making changes, run tests by issuing:
|
||||
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
Finally, commit changes and create a [pull request][8] against the default
|
||||
branch for review. At a minimum, there should be no test regressions and
|
||||
additional tests should be added for new functionality.
|
||||
|
||||
> [!TIP]
|
||||
> Code quality checks are enabled for this repository. To run checks manually,
|
||||
> issue `npm run check`. A Git pre-commit hook is also available to run checks
|
||||
> automatically, which can be installed by issuing `npm run install-hooks`.
|
||||
|
||||
## Making Releases
|
||||
|
||||
Making releases is automated by [GitHub Actions][9]. Releases are created from
|
||||
the default branch; as such, tests should be passing at all times.
|
||||
|
||||
To make a release, perform the following in a feature branch:
|
||||
|
||||
1. Create a new section in [CHANGELOG.md][7] for the release, and move items
|
||||
from Unreleased to this section. Links should be updated to point to the
|
||||
correct tags for comparison.
|
||||
|
||||
2. Run `npm version` with the appropriate arguments to bump the version.
|
||||
|
||||
3. Commit outstanding changes by issuing:
|
||||
|
||||
```
|
||||
$ git commit -a -m "Release <version>"
|
||||
```
|
||||
|
||||
4. Push changes and open a pull request. Verify the results of the [CI][10]
|
||||
workflow before merging.
|
||||
|
||||
5. Once merged, pull changes and create a release tag from the default branch
|
||||
branch by issuing:
|
||||
|
||||
```
|
||||
$ git tag -a -m "Release <version>" <version>
|
||||
```
|
||||
|
||||
6. Push the release tag to the remote repository and verify the results of the
|
||||
[Release][11] workflow:
|
||||
|
||||
```
|
||||
$ git push origin --tags
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Release tags must match the version specified in [manifest.json][12]. See the
|
||||
> [Obsidian Docs][13] for more details.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to this repository, you agree that your contributions will be
|
||||
licensed under its Simplified BSD License.
|
||||
|
||||
[1]: https://github.com/sstallion/obsidian-command-line/issues
|
||||
[2]: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||
[3]: https://nodejs.org/en/about/previous-releases
|
||||
[4]: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo
|
||||
[5]: https://docs.obsidian.md/Reference/Manifest
|
||||
[6]: https://github.com/sstallion/obsidian-command-line/blob/master/package.json
|
||||
[7]: https://github.com/sstallion/obsidian-command-line/blob/master/CHANGELOG.md
|
||||
[8]: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
[9]: https://docs.github.com/en/actions
|
||||
[10]: https://github.com/sstallion/obsidian-command-line/actions/workflows/ci.yml
|
||||
[11]: https://github.com/sstallion/obsidian-command-line/actions/workflows/release.yml
|
||||
[12]: https://github.com/sstallion/obsidian-command-line/blob/master/manifest.json
|
||||
[13]: https://docs.obsidian.md/Plugins/Releasing/Submit+your+plugin#Step%202%20Create%20a%20release
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
61
README.md
Normal file
61
README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Obsidian Command Line
|
||||
|
||||
[][1]
|
||||
[][2]
|
||||
[][3]
|
||||
[][4]
|
||||
|
||||
This plugin adds enhanced support for command line code blocks in Obsidian.
|
||||
|
||||
## Features
|
||||
|
||||
- Copy command lines from your notes without selecting text.
|
||||
- Prompts and output are removed from clipboard contents to support pasting into
|
||||
terminal sessions.
|
||||
- Default prompts are provided for command lines that do not specify one.
|
||||
- Optional syntax highlighting and styling using [Prism Command Line][5].
|
||||
- Optional support for removing unnecessary white space from clipboard contents.
|
||||
- Supports mobile and desktop.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Command line code blocks are identified using one of the following languages:
|
||||
|
||||
| Name | Highlight | Prompt Pattern | Default Prompt | Continuation |
|
||||
| -------------------- | ------------ | -------------- | -------------- | ------------ |
|
||||
| `powershell-command` | `powershell` | `/\S*?>/` | `PS>` | `` ` `` |
|
||||
| `shell-command` | `shell` | `/\S*?[#$%]/` | `$` | `\` |
|
||||
| `windows-command` | `batch` | `/\S*?>/` | `C:\>` | `^` |
|
||||
|
||||
For example, copying the following code block to the clipboard will remove the
|
||||
prompt and output while preserving line continuations and white space:
|
||||
|
||||
````
|
||||
```shell-command
|
||||
$ command with \
|
||||
line continuation
|
||||
and output, which will be removed
|
||||
from the clipboard.
|
||||
```
|
||||
````
|
||||
|
||||
> [!NOTE]
|
||||
> If clipboard normalization is enabled in Settings, unnecessary white space
|
||||
> (including line continuations) will be removed in addition to the prompt and
|
||||
> output.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome! See [CONTRIBUTING.md][6] for details.
|
||||
|
||||
## License
|
||||
|
||||
Source code in this repository is licensed under a Simplified BSD License. See
|
||||
[LICENSE][4] for details.
|
||||
|
||||
[1]: https://github.com/sstallion/obsidian-command-line/actions/workflows/ci.yml
|
||||
[2]: https://github.com/sstallion/obsidian-command-line/blob/master/manifest.json
|
||||
[3]: https://github.com/sstallion/obsidian-command-line/releases/latest
|
||||
[4]: https://github.com/sstallion/obsidian-command-line/blob/master/LICENSE
|
||||
[5]: https://prismjs.com/plugins/command-line/
|
||||
[6]: https://github.com/sstallion/obsidian-command-line/blob/master/CONTRIBUTING.md
|
||||
52
esbuild.config.mjs
Normal file
52
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { http } from "@hyrious/esbuild-plugin-http";
|
||||
import esbuild from "esbuild";
|
||||
import process from "node:process";
|
||||
|
||||
const options = {
|
||||
sourcemap: "inline",
|
||||
format: "cjs",
|
||||
target: "es2023",
|
||||
logLevel: "info",
|
||||
bundle: true,
|
||||
outdir: ".",
|
||||
external: [
|
||||
"obsidian",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
],
|
||||
entryPoints: ["src/main.ts", "src/styles.css"],
|
||||
plugins: [http()],
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
switch (args[0]) {
|
||||
case "release": {
|
||||
Object.assign(options, {
|
||||
sourcemap: false,
|
||||
minify: true,
|
||||
treeShaking: true,
|
||||
});
|
||||
// fallthrough
|
||||
}
|
||||
case "build": {
|
||||
await esbuild.build(options);
|
||||
break;
|
||||
}
|
||||
case "watch": {
|
||||
const context = await esbuild.context(options);
|
||||
await context.watch();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown command: ${args[0]}`);
|
||||
}
|
||||
}
|
||||
25
eslint.config.mjs
Normal file
25
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import eslint from "@eslint/js";
|
||||
import gitignore from "eslint-config-flat-gitignore";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
gitignore(),
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.mjs"],
|
||||
extends: [tseslint.configs.disableTypeChecked],
|
||||
},
|
||||
);
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "command-line",
|
||||
"name": "Command Line",
|
||||
"version": "0.0.0",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Copy command lines from your notes to the clipboard.",
|
||||
"author": "Steven Stallion",
|
||||
"authorUrl": "https://github.com/sstallion",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
4922
package-lock.json
generated
Normal file
4922
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
package.json
Normal file
37
package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "obsidian-command-line",
|
||||
"version": "0.0.0",
|
||||
"license": "BSD-2-Clause",
|
||||
"scripts": {
|
||||
"install-hooks": "simple-git-hooks",
|
||||
"build": "node esbuild.config.mjs build",
|
||||
"release": "node esbuild.config.mjs release",
|
||||
"watch": "node esbuild.config.mjs watch",
|
||||
"test": "vitest",
|
||||
"lint": "eslint",
|
||||
"format": "prettier --write .",
|
||||
"check": "tsc --noEmit && eslint --max-warnings 0 && prettier --check .",
|
||||
"version": "node version.config.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-http": "^0.1.0",
|
||||
"@tsconfig/node20": "^20.0.0",
|
||||
"@types/prismjs": "^1.0.0",
|
||||
"editorconfig": "^2.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-flat-gitignore": "^2.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"obsidian": "latest",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-organize-imports": "latest",
|
||||
"simple-git-hooks": "latest",
|
||||
"tslib": "latest",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 >=20.11.0 || 21 >=21.2.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
3
prettier.config.mjs
Normal file
3
prettier.config.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
plugins: ["prettier-plugin-organize-imports"],
|
||||
};
|
||||
3
simple-git-hooks.json
Normal file
3
simple-git-hooks.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pre-commit": "npm run check"
|
||||
}
|
||||
144
src/command.ts
Normal file
144
src/command.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import * as util from "./util";
|
||||
|
||||
const CONTINUATION_PROMPT = " ";
|
||||
const CONTINUATION_PREFIX = "(con)";
|
||||
const OUTPUT_PREFIX = "(out)";
|
||||
|
||||
export interface CommandLineContext {
|
||||
defaultPrompt: string;
|
||||
promptPattern: string;
|
||||
continuationPattern: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export class CommandLine {
|
||||
private readonly ctx: CommandLineContext;
|
||||
prompt: string | undefined;
|
||||
command: string[];
|
||||
output: string[];
|
||||
|
||||
constructor(
|
||||
ctx: CommandLineContext,
|
||||
prompt: string | undefined,
|
||||
command: string[],
|
||||
output: string[],
|
||||
) {
|
||||
this.ctx = ctx;
|
||||
this.prompt = prompt;
|
||||
this.command = command;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
isMultiline(): boolean {
|
||||
return this.command.length > 0;
|
||||
}
|
||||
|
||||
hasOutput(): boolean {
|
||||
return this.output.length > 0;
|
||||
}
|
||||
|
||||
highlight(codeEl: HTMLElement) {
|
||||
const preEl = codeEl.parentElement!;
|
||||
preEl.addClass("command-line");
|
||||
preEl.setAttrs({
|
||||
"data-prompt": this.prompt ?? this.ctx.defaultPrompt,
|
||||
"data-continuation-prompt": CONTINUATION_PROMPT,
|
||||
"data-filter-continuation": CONTINUATION_PREFIX,
|
||||
"data-filter-output": OUTPUT_PREFIX,
|
||||
});
|
||||
codeEl.setText(this.toCode());
|
||||
}
|
||||
|
||||
normalize(): string {
|
||||
const re = new RegExp(this.ctx.continuationPattern);
|
||||
return this.command.map((line) => line.replace(re, "").trim()).join(" ");
|
||||
}
|
||||
|
||||
toCode(): string {
|
||||
return util
|
||||
.mapTail(this.command, (line) => CONTINUATION_PREFIX + line)
|
||||
.concat(this.output.map((line) => OUTPUT_PREFIX + line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.command.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const enum CommandLineParserState {
|
||||
Prompt,
|
||||
Command,
|
||||
Output,
|
||||
}
|
||||
|
||||
export class CommandLineParser {
|
||||
private readonly ctx: CommandLineContext;
|
||||
private readonly re: Record<string, RegExp>;
|
||||
|
||||
constructor(ctx: CommandLineContext) {
|
||||
this.ctx = ctx;
|
||||
this.re = {
|
||||
prompt: new RegExp(ctx.promptPattern),
|
||||
continuation: new RegExp(ctx.continuationPattern),
|
||||
};
|
||||
}
|
||||
|
||||
parse(text: string): CommandLine {
|
||||
let line, prompt;
|
||||
const command = [];
|
||||
const output = [];
|
||||
let state = CommandLineParserState.Prompt;
|
||||
const lines = text.trim().split("\n");
|
||||
while ((line = lines.shift())) {
|
||||
switch (state) {
|
||||
case CommandLineParserState.Prompt: {
|
||||
const match = line.match(this.re.prompt);
|
||||
if (match) {
|
||||
prompt = match[0].trim();
|
||||
line = line.replace(this.re.prompt, "");
|
||||
}
|
||||
state = CommandLineParserState.Command;
|
||||
// fallthrough
|
||||
}
|
||||
case CommandLineParserState.Command: {
|
||||
command.push(line);
|
||||
if (!line.match(this.re.continuation)) {
|
||||
state = CommandLineParserState.Output;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CommandLineParserState.Output: {
|
||||
output.push(line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CommandLine(this.ctx, prompt, command, output);
|
||||
}
|
||||
}
|
||||
160
src/main.ts
Normal file
160
src/main.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import { ButtonComponent, Plugin } from "obsidian";
|
||||
import { CommandLine, CommandLineParser } from "./command";
|
||||
import { CommandLineSettings, CommandLineSettingsTab } from "./settings";
|
||||
import * as util from "./util";
|
||||
|
||||
const COPY_BUTTON_DELAY = 1000;
|
||||
|
||||
const DEFAULT_SETTINGS: Partial<CommandLineSettings> = {
|
||||
highlight: false,
|
||||
normalize: false,
|
||||
languages: {
|
||||
"powershell-command": {
|
||||
defaultPrompt: "PS>",
|
||||
promptPattern: "^\\S*?>\\s*",
|
||||
continuationPattern: "`$",
|
||||
alias: "powershell",
|
||||
},
|
||||
"shell-command": {
|
||||
defaultPrompt: "$",
|
||||
promptPattern: "^\\S*?[#$%]\\s*",
|
||||
continuationPattern: "\\\\$",
|
||||
alias: "shell",
|
||||
},
|
||||
"windows-command": {
|
||||
defaultPrompt: "C:\\>",
|
||||
promptPattern: "^\\S*?>\\s*",
|
||||
continuationPattern: "\\^$",
|
||||
alias: "batch",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default class CommandLinePlugin extends Plugin {
|
||||
// @ts-ignore: Initialized by onload()
|
||||
settings: CommandLineSettings;
|
||||
|
||||
addCopyButton(containerEl: HTMLElement, command: CommandLine): ButtonComponent {
|
||||
return new ButtonComponent(containerEl)
|
||||
.setIcon("lucide-copy")
|
||||
.setClass("copy-code-button")
|
||||
.then((button) => {
|
||||
button.onClick(async (evt) => {
|
||||
evt.preventDefault();
|
||||
await navigator.clipboard.writeText(
|
||||
this.settings.normalize
|
||||
? command.normalize()
|
||||
: command.toString(),
|
||||
);
|
||||
button // prettier-ignore
|
||||
.setIcon("lucide-check")
|
||||
.buttonEl.setCssStyles({
|
||||
color: "var(--text-success)",
|
||||
display: "inline-flex",
|
||||
});
|
||||
setTimeout(() => {
|
||||
button // prettier-ignore
|
||||
.setIcon("lucide-copy")
|
||||
.buttonEl.setCssStyles({
|
||||
color: "",
|
||||
display: "",
|
||||
});
|
||||
}, COPY_BUTTON_DELAY);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const settings = (await this.loadData()) as CommandLineSettings;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, settings);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
async onExternalSettingsChange() {
|
||||
await this.loadSettings();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.addSettingTab(new CommandLineSettingsTab(this));
|
||||
|
||||
// Obsidian loads Prism asynchronously, however it does not bundle
|
||||
// additional plugins. A deferred dynamic import is needed to avoid a
|
||||
// race when registering plugins. Care must be taken to import plugins
|
||||
// from a compatible version of Prism for a given Obsidian release.
|
||||
if (this.settings.highlight) {
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
await util.waitForGlobal("Prism").then(async () => {
|
||||
await import(
|
||||
// @ts-ignore: Bundled by esbuild
|
||||
"https://unpkg.com/prismjs@v1.29.0/plugins/command-line/prism-command-line.js"
|
||||
);
|
||||
const entries = Object.entries(this.settings.languages);
|
||||
for (const [language, ctx] of entries) {
|
||||
if (ctx?.alias) {
|
||||
Prism.languages[language] = Prism.languages[ctx.alias];
|
||||
}
|
||||
Prism.languages[language] ??= {};
|
||||
}
|
||||
Prism.highlightAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.registerMarkdownPostProcessor((el) => {
|
||||
for (const codeEl of el.findAll("pre > code")) {
|
||||
const language = util.getLanguage(codeEl);
|
||||
if (!(language in this.settings.languages)) {
|
||||
continue; // unsupported language
|
||||
}
|
||||
|
||||
const ctx = this.settings.languages[language];
|
||||
const command = new CommandLineParser(ctx).parse(codeEl.getText());
|
||||
if (this.settings.highlight) {
|
||||
command.highlight(codeEl);
|
||||
}
|
||||
|
||||
// Due to the order in which builtin post processors are
|
||||
// registered, there does not appear to be a better method of
|
||||
// modifying clipboard behavior. Rather than fight event
|
||||
// handling, we replace the copy button a facsimile.
|
||||
const preEl = codeEl.parentElement!;
|
||||
const buttonEl = preEl.find(".copy-code-button");
|
||||
if (buttonEl) {
|
||||
const button = this.addCopyButton(preEl, command);
|
||||
buttonEl.replaceWith(button.buttonEl);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
83
src/settings.ts
Normal file
83
src/settings.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import { PluginSettingTab, Setting } from "obsidian";
|
||||
import { CommandLineContext } from "./command";
|
||||
import CommandLinePlugin from "./main";
|
||||
import * as util from "./util";
|
||||
|
||||
const RELOAD_DELAY = 500;
|
||||
|
||||
export interface CommandLineSettings {
|
||||
highlight: boolean;
|
||||
normalize: boolean;
|
||||
languages: Record<string, CommandLineContext>;
|
||||
}
|
||||
|
||||
export class CommandLineSettingsTab extends PluginSettingTab {
|
||||
readonly plugin: CommandLinePlugin;
|
||||
|
||||
constructor(plugin: CommandLinePlugin) {
|
||||
super(plugin.app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Highlight command lines")
|
||||
.setDesc("Enable highlighting and additional styling for command lines.")
|
||||
.then(({ descEl }) => {
|
||||
descEl
|
||||
.createDiv("mod-warning")
|
||||
.setText("Changing this option will reload the app.");
|
||||
})
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.highlight)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.highlight = value;
|
||||
await this.plugin.saveSettings();
|
||||
await util.sleep(RELOAD_DELAY).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Normalize clipboard contents")
|
||||
.setDesc("Remove uneccessary white space when copying to the clipboard.")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.normalize)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.normalize = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
26
src/styles.css
Normal file
26
src/styles.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
@import "https://unpkg.com/prismjs@v1.29.0/plugins/command-line/prism-command-line.css";
|
||||
61
src/util.ts
Normal file
61
src/util.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
export function getLanguage(el: HTMLElement): string {
|
||||
for (const cls of el.classList) {
|
||||
const result = /^language-(.*)$/.exec(cls);
|
||||
if (result) {
|
||||
return result[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function mapTail<T>(array: T[], callbackFn: (thisArg: T) => T): T[] {
|
||||
return array.reduce((accumulator, currentValue) => {
|
||||
if (accumulator.length == 0) {
|
||||
accumulator.push(currentValue);
|
||||
} else {
|
||||
accumulator.push(callbackFn(currentValue));
|
||||
}
|
||||
return accumulator;
|
||||
}, [] as T[]);
|
||||
}
|
||||
|
||||
export function sleep(delay: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
export function waitForGlobal(name: string, delay = 100): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
(function resolver() {
|
||||
if (name in window) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(resolver, delay);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
79
tests/command.test.ts
Normal file
79
tests/command.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import { test as base, expect } from "vitest";
|
||||
import { CommandLineParser } from "../src/command";
|
||||
|
||||
const test = base.extend({
|
||||
parser: new CommandLineParser({
|
||||
defaultPrompt: "$",
|
||||
promptPattern: "^\\$\\s*",
|
||||
continuationPattern: "\\\\$",
|
||||
}),
|
||||
});
|
||||
|
||||
test("parse empty string", ({ parser }) => {
|
||||
const actual = parser.parse("");
|
||||
expect(actual.toString()).toBe("");
|
||||
});
|
||||
|
||||
test("parse with prompt", ({ parser }) => {
|
||||
const actual = parser.parse("$ command with prompt");
|
||||
expect(actual.toString()).toBe("command with prompt");
|
||||
});
|
||||
|
||||
test("parse with continuations", ({ parser }) => {
|
||||
const actual = parser.parse("command \\\nwith \\\ncontinuations");
|
||||
expect(actual.toString()).toBe("command \\\nwith \\\ncontinuations");
|
||||
});
|
||||
|
||||
test("parse with output", ({ parser }) => {
|
||||
const actual = parser.parse("command\nwith\noutput");
|
||||
expect(actual.toString()).toBe("command"); // with output
|
||||
});
|
||||
|
||||
test("normalize with continuations", ({ parser }) => {
|
||||
const actual = parser.parse("command \\\nwith \\\ncontinuations");
|
||||
expect(actual.normalize()).toBe("command with continuations");
|
||||
});
|
||||
|
||||
test("normalize with inner white space", ({ parser }) => {
|
||||
const actual = parser.parse("command with inner white space preserved");
|
||||
expect(actual.normalize()).toBe("command with inner white space preserved");
|
||||
});
|
||||
|
||||
test("normalize with outer white space", ({ parser }) => {
|
||||
const actual = parser.parse(" command with outer white space removed ");
|
||||
expect(actual.normalize()).toBe("command with outer white space removed");
|
||||
});
|
||||
|
||||
test("normalize the kitchen sink", ({ parser }) => {
|
||||
const actual = parser.parse(`$ command with \\
|
||||
entirely too \\
|
||||
much formatting
|
||||
and a couple lines
|
||||
of output\n`);
|
||||
expect(actual.normalize()).toBe("command with entirely too much formatting");
|
||||
});
|
||||
66
tests/util.test.ts
Normal file
66
tests/util.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Steven Stallion <sstallion@gmail.com>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import * as util from "../src/util";
|
||||
|
||||
describe("getLanguage()", () => {
|
||||
test("defaults to empty string", () => {
|
||||
const codeEl = document.createElement("code");
|
||||
expect(util.getLanguage(codeEl)).toBe("");
|
||||
});
|
||||
|
||||
test("returns language", () => {
|
||||
const codeEl = document.createElement("code");
|
||||
codeEl.classList.add("language-test");
|
||||
expect(util.getLanguage(codeEl)).toBe("test");
|
||||
});
|
||||
|
||||
test("returns first language in list", () => {
|
||||
const codeEl = document.createElement("code");
|
||||
for (const name of ["first", "next", "last"]) {
|
||||
codeEl.classList.add(`language-${name}`);
|
||||
}
|
||||
expect(util.getLanguage(codeEl)).toBe("first");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapTail()", () => {
|
||||
test("defaults to empty array", () => {
|
||||
expect(util.mapTail([], vi.fn())).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test("accumulates head and tail elements", () => {
|
||||
expect(
|
||||
util.mapTail(["head", "", ""], (value) => value + "tail"),
|
||||
).toStrictEqual(["head", "tail", "tail"]);
|
||||
});
|
||||
|
||||
test("skips callbacks without tail elements", () => {
|
||||
const callbackFn = vi.fn();
|
||||
util.mapTail(["head"], callbackFn);
|
||||
expect(callbackFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowUmdGlobalAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"lib": ["dom", "dom.iterable", "es2023"]
|
||||
},
|
||||
"include": ["src/**/*", "tests/**/*"]
|
||||
}
|
||||
26
version.config.mjs
Normal file
26
version.config.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import editorconfig from "editorconfig";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { env } from "node:process";
|
||||
|
||||
// Update manifest.json and versions.json based on the package version.
|
||||
// EditorConfig rules are applied to ensure consistent formatting.
|
||||
const version = env.npm_package_version;
|
||||
|
||||
function readJSON(path) {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function writeJSON(path, value) {
|
||||
const config = editorconfig.parseSync(path);
|
||||
const space = config.indent_style === "tab" ? "\t" : (config.indent_size ?? 2);
|
||||
const newline = config.insert_final_newline ? "\n" : "";
|
||||
writeFileSync(path, JSON.stringify(value, null, space) + newline);
|
||||
}
|
||||
|
||||
const manifest = readJSON("manifest.json");
|
||||
manifest.version = version;
|
||||
writeJSON("manifest.json", manifest);
|
||||
|
||||
const versions = readJSON("versions.json");
|
||||
versions[version] = manifest.minAppVersion;
|
||||
writeJSON("versions.json", versions);
|
||||
1
versions.json
Normal file
1
versions.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
7
vitest.config.mjs
Normal file
7
vitest.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue