mirror of
https://github.com/ruszabarov/obsidian-unit-converter.git
synced 2026-07-22 05:31:40 +00:00
feat: live preview
This commit is contained in:
parent
b39c0a4d6e
commit
2b9a4fc0b3
14 changed files with 488 additions and 217 deletions
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
|
|
@ -1,24 +1,29 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
|
|
|||
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
|
|
@ -1,33 +1,38 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
- name: Build plugin
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
main.js manifest.json styles.css
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
main.js manifest.json styles.css
|
||||
|
|
|
|||
146
README.md
146
README.md
|
|
@ -1,73 +1,73 @@
|
|||
# Unit Conversion in Obsidian
|
||||
|
||||
This plugin allows you to seamlessly convert units directly within your Obsidian notes, making it easier to work with data across various systems and contexts without leaving your markdown editor.
|
||||
|
||||
## Basic Syntax
|
||||
Use the following syntax within your notes to perform inline conversion:
|
||||
`[<value><source unit>|<target unit>]`
|
||||
|
||||
## Example
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/a191b540-decd-4f2e-930b-0aa8a9f71678" alt="obsidian-unit-converter" width="500">
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- [Autosuggest](#autosuggest)
|
||||
- [Convert units command](#convert-units-command)
|
||||
|
||||
### Autosuggest
|
||||
|
||||
The plugin will automatically suggest relevant units. This option can be disabled in settings.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/b6988894-83da-4a76-8ca9-87f335d72082" alt="autosuggest example" width="200">
|
||||
|
||||
### Convert units command
|
||||
|
||||
You can open a modal to perform a unit conversion. This is mostly done to improve the mobile experience.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/ca168075-8408-41e3-812f-181c511b1585" alt="convert units command example" width="350">
|
||||
|
||||
## Supported Units
|
||||
|
||||
**Length**: mm, cm, m, in, ft-us, ft, mi
|
||||
|
||||
**Area**: mm2 cm2, m2, ha, km2, in2, ft2, ac, mi2
|
||||
|
||||
**Mass**: mcg, mg, g, kg, oz, lb, mt, t
|
||||
|
||||
**Volume**: mm3, cm3, ml, l, kl, m3, km3, tsp, Tbs, in3, fl-oz, cup, pnt, qt, gal, ft3, yd3
|
||||
|
||||
**Volume Flow Rate**: mm3/s, cm3/s, ml/s, cl/s, dl/s, l/s, l/min, l/h, kl/s, kl/min, kl/h, m3/s, m3/min, m3/h, km3/s, tsp/s, Tbs/s, in3/s, in3/min, in3/h, fl-oz/s, fl-oz/min, fl-oz/h, cup/s, pnt/s, pnt/min, pnt/h, qt/s, gal/s, gal/min, gal/h, ft3/s, ft3/min, ft3/h, yd3/s, yd3/min, yd3/h'
|
||||
|
||||
**Temperature**: C, F, K, R
|
||||
|
||||
**Time**: ns, mu, ms, s, min, h, d, week, month, year
|
||||
|
||||
**Frequency**: Hz, mHz, kHz, MHz, GHz, THz, rpm, deg/s, rad/s
|
||||
|
||||
**Speed**: m/s, km/h, m/h, knot, ft/s
|
||||
|
||||
**Pace**: s/m, min/km, s/ft, min/km
|
||||
|
||||
**Pressure**: Pa, hPa, kPa, MPa, bar, torr, psi, ksi
|
||||
|
||||
**Digital**: b, Kb, Mb, Gb, Tb, B, KB, MB, GB, TB
|
||||
|
||||
**Illuminance**: lx, ft-cd, Parts-Per, ppm, ppb, ppt, ppq
|
||||
|
||||
**Voltage**: V, mV, kV
|
||||
|
||||
**Current**: A, mA, kA
|
||||
|
||||
**Power**: W, mW, kW, MW, GW
|
||||
|
||||
**Apparent Power**: VA, mVA, kVA, MVA, GVA
|
||||
|
||||
**Reactive Power**: VAR, mVAR, kVAR, MVAR, GVAR
|
||||
|
||||
**Energy**: Wh, mWh, kWh, MWh, GWh, J, kJ
|
||||
|
||||
**Reactive Energy**: VARh, mVARh, kVARh, MVARh, GVARh
|
||||
|
||||
**Angle**: deg, rad, grad, arcmin, arcsec
|
||||
# Unit Conversion in Obsidian
|
||||
|
||||
This plugin allows you to seamlessly convert units directly within your Obsidian notes, making it easier to work with data across various systems and contexts without leaving your markdown editor.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Use the following syntax within your notes to perform inline conversion:
|
||||
`[<value><source unit>|<target unit>]`
|
||||
|
||||
## Example
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/a191b540-decd-4f2e-930b-0aa8a9f71678" alt="obsidian-unit-converter" width="500">
|
||||
|
||||
## Features
|
||||
|
||||
- [Autosuggest](#autosuggest)
|
||||
- [Convert units command](#convert-units-command)
|
||||
|
||||
### Autosuggest
|
||||
|
||||
The plugin will automatically suggest relevant units. This option can be disabled in settings.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/b6988894-83da-4a76-8ca9-87f335d72082" alt="autosuggest example" width="200">
|
||||
|
||||
### Convert units command
|
||||
|
||||
You can open a modal to perform a unit conversion. This is mostly done to improve the mobile experience.
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/ca168075-8408-41e3-812f-181c511b1585" alt="convert units command example" width="350">
|
||||
|
||||
## Supported Units
|
||||
|
||||
**Length**: mm, cm, m, in, ft-us, ft, mi
|
||||
|
||||
**Area**: mm2 cm2, m2, ha, km2, in2, ft2, ac, mi2
|
||||
|
||||
**Mass**: mcg, mg, g, kg, oz, lb, mt, t
|
||||
|
||||
**Volume**: mm3, cm3, ml, l, kl, m3, km3, tsp, Tbs, in3, fl-oz, cup, pnt, qt, gal, ft3, yd3
|
||||
|
||||
**Volume Flow Rate**: mm3/s, cm3/s, ml/s, cl/s, dl/s, l/s, l/min, l/h, kl/s, kl/min, kl/h, m3/s, m3/min, m3/h, km3/s, tsp/s, Tbs/s, in3/s, in3/min, in3/h, fl-oz/s, fl-oz/min, fl-oz/h, cup/s, pnt/s, pnt/min, pnt/h, qt/s, gal/s, gal/min, gal/h, ft3/s, ft3/min, ft3/h, yd3/s, yd3/min, yd3/h'
|
||||
|
||||
**Temperature**: C, F, K, R
|
||||
|
||||
**Time**: ns, mu, ms, s, min, h, d, week, month, year
|
||||
|
||||
**Frequency**: Hz, mHz, kHz, MHz, GHz, THz, rpm, deg/s, rad/s
|
||||
|
||||
**Speed**: m/s, km/h, m/h, knot, ft/s
|
||||
|
||||
**Pace**: s/m, min/km, s/ft, min/km
|
||||
|
||||
**Pressure**: Pa, hPa, kPa, MPa, bar, torr, psi, ksi
|
||||
|
||||
**Digital**: b, Kb, Mb, Gb, Tb, B, KB, MB, GB, TB
|
||||
|
||||
**Illuminance**: lx, ft-cd, Parts-Per, ppm, ppb, ppt, ppq
|
||||
|
||||
**Voltage**: V, mV, kV
|
||||
|
||||
**Current**: A, mA, kA
|
||||
|
||||
**Power**: W, mW, kW, MW, GW
|
||||
|
||||
**Apparent Power**: VA, mVA, kVA, MVA, GVA
|
||||
|
||||
**Reactive Power**: VAR, mVAR, kVAR, MVAR, GVAR
|
||||
|
||||
**Energy**: Wh, mWh, kWh, MWh, GWh, J, kJ
|
||||
|
||||
**Reactive Energy**: VARh, mVARh, kVARh, MVARh, GVARh
|
||||
|
||||
**Angle**: deg, rad, grad, arcmin, arcsec
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
"description": "Converts units right in your Markdown files.",
|
||||
"author": "Ruslan Zabarov",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"author": "Ruslan Zabarov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.3",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/convert-units": "^2.3.11",
|
||||
"@types/node": "^22.13.1",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ importers:
|
|||
specifier: ^2.3.4
|
||||
version: 2.3.4
|
||||
devDependencies:
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
'@codemirror/view':
|
||||
specifier: ^6.36.3
|
||||
version: 6.36.3
|
||||
'@eslint/js':
|
||||
specifier: ^9.16.0
|
||||
version: 9.21.0
|
||||
|
|
@ -38,7 +44,7 @@ importers:
|
|||
version: 16.0.0
|
||||
obsidian:
|
||||
specifier: latest
|
||||
version: 1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)
|
||||
version: 1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.3)
|
||||
rolldown:
|
||||
specifier: ^1.0.0-beta.3
|
||||
version: 1.0.0-beta.3(typescript@5.7.3)
|
||||
|
|
@ -54,11 +60,11 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@codemirror/state@6.4.1':
|
||||
resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==}
|
||||
'@codemirror/state@6.5.2':
|
||||
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
|
||||
|
||||
'@codemirror/view@6.35.0':
|
||||
resolution: {integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==}
|
||||
'@codemirror/view@6.36.3':
|
||||
resolution: {integrity: sha512-N2bilM47QWC8Hnx0rMdDxO2x2ImJ1FvZWXubwKgjeoOrWwEiFrtpA7SFHcuZ+o2Ze2VzbkgbzWVj4+V18LVkeg==}
|
||||
|
||||
'@emnapi/core@1.3.1':
|
||||
resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==}
|
||||
|
|
@ -123,6 +129,9 @@ packages:
|
|||
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.6':
|
||||
resolution: {integrity: sha512-z8YVS3XszxFTO73iwvFDNpQIzdMmSDTP/mB3E/ucR37V3Sx57hSExcXyMoNwaucWxnsWf4xfbZv0iZ30jr0M4Q==}
|
||||
|
||||
|
|
@ -737,11 +746,13 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@codemirror/state@6.4.1': {}
|
||||
|
||||
'@codemirror/view@6.35.0':
|
||||
'@codemirror/state@6.5.2':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.1
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/view@6.36.3':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
style-mod: 4.1.2
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
|
|
@ -816,6 +827,8 @@ snapshots:
|
|||
|
||||
'@humanwhocodes/retry@0.4.2': {}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.6':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.3.1
|
||||
|
|
@ -1318,10 +1331,10 @@ snapshots:
|
|||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
obsidian@1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.35.0):
|
||||
obsidian@1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.3):
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/view': 6.35.0
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.3
|
||||
'@types/codemirror': 5.60.8
|
||||
moment: 2.29.4
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
export default {
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
dir: ".",
|
||||
sourcemap: true,
|
||||
format: "cjs",
|
||||
exports: "default",
|
||||
banner: 'if (typeof window !== "undefined" && typeof window.global === "undefined") { window.global = window; }'
|
||||
},
|
||||
external: ["obsidian"],
|
||||
esbuild: {
|
||||
target: "es2018",
|
||||
platform: "browser",
|
||||
format: "cjs",
|
||||
bundle: true,
|
||||
sourcemap: true
|
||||
}
|
||||
};
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
dir: ".",
|
||||
sourcemap: true,
|
||||
format: "cjs",
|
||||
exports: "default",
|
||||
banner: 'if (typeof window !== "undefined" && typeof window.global === "undefined") { window.global = window; }',
|
||||
},
|
||||
external: ["obsidian", "@codemirror/state", "@codemirror/view"],
|
||||
esbuild: {
|
||||
target: "es2018",
|
||||
platform: "browser",
|
||||
format: "cjs",
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
136
src/editor/extension.ts
Normal file
136
src/editor/extension.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import {
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { Unit } from "convert-units";
|
||||
import UnitConverterPlugin from "../main";
|
||||
import {
|
||||
convertValue,
|
||||
formatConversion,
|
||||
CONVERSION_REGEX,
|
||||
} from "../utils/conversion";
|
||||
|
||||
type EditorMode = "source" | "live-preview";
|
||||
|
||||
class ConversionWidget extends WidgetType {
|
||||
constructor(
|
||||
private readonly value: number,
|
||||
private readonly fromUnit: Unit,
|
||||
private readonly toUnit: Unit,
|
||||
private readonly plugin: UnitConverterPlugin
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const span = document.createElement("span");
|
||||
|
||||
span.textContent = formatConversion(
|
||||
this.value,
|
||||
this.fromUnit,
|
||||
this.toUnit,
|
||||
this.plugin.settings.useDescriptiveNames
|
||||
);
|
||||
|
||||
return span;
|
||||
}
|
||||
}
|
||||
|
||||
export function createUnitConversionExtension(plugin: UnitConverterPlugin) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
currentEditorMode: EditorMode = "source";
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.currentEditorMode = this.detectEditorMode(view);
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
detectEditorMode(view: EditorView): EditorMode {
|
||||
const editorElement = view.dom.closest(
|
||||
".markdown-source-view.mod-cm6"
|
||||
);
|
||||
return editorElement?.classList.contains("is-live-preview")
|
||||
? "live-preview"
|
||||
: "source";
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
const newMode = this.detectEditorMode(update.view);
|
||||
const modeChanged = newMode !== this.currentEditorMode;
|
||||
|
||||
if (
|
||||
update.docChanged ||
|
||||
update.selectionSet ||
|
||||
update.viewportChanged ||
|
||||
modeChanged
|
||||
) {
|
||||
this.currentEditorMode = newMode;
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
if (
|
||||
!plugin.settings.livePreviewInEditMode ||
|
||||
this.currentEditorMode === "source"
|
||||
) {
|
||||
return Decoration.none;
|
||||
}
|
||||
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
|
||||
const cursorPos = view.state.selection.main.head;
|
||||
const cursorLine = view.state.doc.lineAt(cursorPos).number;
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
const text = view.state.doc.sliceString(from, to);
|
||||
let match;
|
||||
|
||||
while ((match = CONVERSION_REGEX.exec(text)) !== null) {
|
||||
const start = from + match.index;
|
||||
const end = start + match[0].length;
|
||||
const lineAtMatch = view.state.doc.lineAt(start).number;
|
||||
|
||||
// Apply decoration if we're not on the active line
|
||||
if (lineAtMatch !== cursorLine) {
|
||||
const value = parseFloat(match[1]);
|
||||
const fromUnit = match[2] as Unit;
|
||||
const toUnit = match[3] as Unit;
|
||||
|
||||
try {
|
||||
convertValue(value, fromUnit, toUnit);
|
||||
|
||||
builder.add(
|
||||
start,
|
||||
end,
|
||||
Decoration.replace({
|
||||
widget: new ConversionWidget(
|
||||
value,
|
||||
fromUnit,
|
||||
toUnit,
|
||||
plugin
|
||||
),
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.debug("Invalid conversion:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
}
|
||||
);
|
||||
}
|
||||
50
src/editor/md-post-processor.ts
Normal file
50
src/editor/md-post-processor.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { MarkdownPostProcessor } from "obsidian";
|
||||
import { Unit } from "convert-units";
|
||||
import { UnitConverterSettings } from "../settings";
|
||||
import { CONVERSION_REGEX, formatConversion } from "../utils/conversion";
|
||||
|
||||
export function createMarkdownPostProcessor(
|
||||
settings: UnitConverterSettings
|
||||
): MarkdownPostProcessor {
|
||||
return (element: HTMLElement) => {
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null
|
||||
);
|
||||
let node = walker.nextNode();
|
||||
|
||||
while (node) {
|
||||
const text = node.nodeValue;
|
||||
|
||||
if (!text) {
|
||||
node = walker.nextNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
const newText = text.replace(
|
||||
CONVERSION_REGEX,
|
||||
(
|
||||
match: string,
|
||||
valueStr: string,
|
||||
fromUnit: Unit,
|
||||
toUnit: Unit
|
||||
) => {
|
||||
const value = parseFloat(valueStr);
|
||||
return formatConversion(
|
||||
value,
|
||||
fromUnit,
|
||||
toUnit,
|
||||
settings.useDescriptiveNames
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (newText !== text) {
|
||||
node.nodeValue = newText;
|
||||
}
|
||||
|
||||
node = walker.nextNode();
|
||||
}
|
||||
};
|
||||
}
|
||||
99
src/main.ts
99
src/main.ts
|
|
@ -1,5 +1,5 @@
|
|||
import { Plugin, Editor } from "obsidian";
|
||||
import convert, { Unit } from "convert-units";
|
||||
import { Unit } from "convert-units";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
UnitConverterSettings,
|
||||
|
|
@ -7,9 +7,13 @@ import {
|
|||
} from "./settings";
|
||||
import DestinationUnitSuggest from "./suggest/to-unit-suggest";
|
||||
import { ConversionModal } from "./modal/conversion-modal";
|
||||
import { createUnitConversionExtension } from "./editor/extension";
|
||||
import { createMarkdownPostProcessor } from "./editor/md-post-processor";
|
||||
import { Extension } from "@codemirror/state";
|
||||
|
||||
export default class UnitConverterPlugin extends Plugin {
|
||||
settings: UnitConverterSettings;
|
||||
private editorExtension: Extension;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -20,6 +24,28 @@ export default class UnitConverterPlugin extends Plugin {
|
|||
this.registerEditorSuggest(new DestinationUnitSuggest(this));
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("layout-change", () => {
|
||||
this.refreshEditorExtensions();
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("active-leaf-change", () => {
|
||||
this.refreshEditorExtensions();
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-change", () => {
|
||||
this.refreshEditorExtensions();
|
||||
})
|
||||
);
|
||||
|
||||
this.registerMarkdownPostProcessor(
|
||||
createMarkdownPostProcessor(this.settings)
|
||||
);
|
||||
|
||||
this.addCommand({
|
||||
id: "convert-units",
|
||||
name: "Convert units",
|
||||
|
|
@ -34,68 +60,19 @@ export default class UnitConverterPlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
this.registerMarkdownPostProcessor((element: HTMLElement) => {
|
||||
const regex = /\[([\d.]+)([a-zA-Z0-9\-/]+)\|([a-zA-Z0-9\-/]+)\]/g;
|
||||
this.refreshEditorExtensions();
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null
|
||||
);
|
||||
let node = walker.nextNode();
|
||||
refreshEditorExtensions() {
|
||||
// Unregister previous extension if it exists
|
||||
if (this.editorExtension) {
|
||||
this.app.workspace.updateOptions();
|
||||
}
|
||||
|
||||
while (node) {
|
||||
const text = node.nodeValue;
|
||||
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newText = text.replace(
|
||||
regex,
|
||||
(match, valueStr, fromUnit, toUnit) => {
|
||||
const value = parseFloat(valueStr);
|
||||
let convertedValue: number;
|
||||
let displayUnit = toUnit;
|
||||
|
||||
try {
|
||||
convertedValue = convert(value)
|
||||
.from(fromUnit)
|
||||
.to(toUnit);
|
||||
|
||||
if (this.settings.useDescriptiveNames) {
|
||||
try {
|
||||
const measure = convert().describe(toUnit);
|
||||
if (measure && measure.plural) {
|
||||
displayUnit = (
|
||||
convertedValue === 1
|
||||
? measure.singular
|
||||
: measure.plural
|
||||
).toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error getting descriptive name:",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Conversion error:", e);
|
||||
return match; // Return the original text if conversion fails
|
||||
}
|
||||
|
||||
return `${convertedValue.toFixed(2)} ${displayUnit}`;
|
||||
}
|
||||
);
|
||||
|
||||
if (newText !== text) {
|
||||
node.nodeValue = newText;
|
||||
}
|
||||
|
||||
node = walker.nextNode();
|
||||
}
|
||||
});
|
||||
if (this.settings.livePreviewInEditMode) {
|
||||
this.editorExtension = createUnitConversionExtension(this);
|
||||
this.registerEditorExtension(this.editorExtension);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import UnitConverterPlugin from "./main";
|
|||
export interface UnitConverterSettings {
|
||||
useDescriptiveNames: boolean;
|
||||
isAutosuggestEnabled: boolean;
|
||||
livePreviewInEditMode: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: UnitConverterSettings = {
|
||||
useDescriptiveNames: false,
|
||||
isAutosuggestEnabled: true,
|
||||
livePreviewInEditMode: true,
|
||||
};
|
||||
|
||||
export class UnitConverterSettingTab extends PluginSettingTab {
|
||||
|
|
@ -48,5 +50,20 @@ export class UnitConverterSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Live Preview Conversions")
|
||||
.setDesc(
|
||||
"Show converted units in Live Preview mode only (not in Source mode)"
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.livePreviewInEditMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.livePreviewInEditMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshEditorExtensions();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import UnitConverterPlugin from "src/main";
|
||||
import UnitConverterPlugin from "src/editor/post-processor";
|
||||
import {
|
||||
Editor,
|
||||
EditorPosition,
|
||||
|
|
@ -20,7 +20,7 @@ export default class DestinationUnitSuggest extends EditorSuggest<DestinationUni
|
|||
|
||||
onTrigger(
|
||||
cursor: EditorPosition,
|
||||
editor: Editor,
|
||||
editor: Editor
|
||||
): EditorSuggestTriggerInfo | null {
|
||||
const line = editor.getLine(cursor.line);
|
||||
const subString = line.substring(0, cursor.ch);
|
||||
|
|
@ -29,8 +29,8 @@ export default class DestinationUnitSuggest extends EditorSuggest<DestinationUni
|
|||
const match = subString.match(/\[([\d.]+)([a-zA-Z0-9\-/]+)\|$/);
|
||||
if (!match) return null;
|
||||
|
||||
const [,, fromUnit] = match;
|
||||
|
||||
const [, , fromUnit] = match;
|
||||
|
||||
try {
|
||||
// Verify if the fromUnit is valid
|
||||
convert().from(fromUnit as Unit);
|
||||
|
|
@ -53,7 +53,9 @@ export default class DestinationUnitSuggest extends EditorSuggest<DestinationUni
|
|||
getSuggestions(context: EditorSuggestContext): DestinationUnitCompletion[] {
|
||||
const fromUnit = context.query;
|
||||
try {
|
||||
const possibilities = convert().from(fromUnit as Unit).possibilities();
|
||||
const possibilities = convert()
|
||||
.from(fromUnit as Unit)
|
||||
.possibilities();
|
||||
return possibilities.map((unit) => {
|
||||
const measure = convert().describe(unit);
|
||||
return {
|
||||
|
|
@ -80,14 +82,10 @@ export default class DestinationUnitSuggest extends EditorSuggest<DestinationUni
|
|||
line: start.line,
|
||||
ch: start.ch + value.value.length,
|
||||
};
|
||||
editor.replaceRange(
|
||||
value.value,
|
||||
start,
|
||||
{
|
||||
line: start.line,
|
||||
ch: start.ch,
|
||||
}
|
||||
);
|
||||
editor.replaceRange(value.value, start, {
|
||||
line: start.line,
|
||||
ch: start.ch,
|
||||
});
|
||||
editor.setCursor(endPos);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
src/utils/conversion.ts
Normal file
68
src/utils/conversion.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import convert, { Unit } from "convert-units";
|
||||
|
||||
/**
|
||||
* Converts a value from one unit to another
|
||||
*/
|
||||
export function convertValue(
|
||||
value: number,
|
||||
fromUnit: Unit,
|
||||
toUnit: Unit
|
||||
): number {
|
||||
return convert(value).from(fromUnit).to(toUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display unit name based on the value and unit
|
||||
*/
|
||||
export function getDisplayUnit(
|
||||
value: number,
|
||||
unit: Unit,
|
||||
useDescriptiveNames: boolean
|
||||
): string {
|
||||
if (!useDescriptiveNames) {
|
||||
return unit.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
const measure = convert().describe(unit);
|
||||
if (measure && measure.plural) {
|
||||
return (
|
||||
value === 1 ? measure.singular : measure.plural
|
||||
).toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error getting descriptive name:", e);
|
||||
}
|
||||
|
||||
return unit.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs unit conversion and returns the formatted result
|
||||
*/
|
||||
export function formatConversion(
|
||||
value: number,
|
||||
fromUnit: Unit,
|
||||
toUnit: Unit,
|
||||
useDescriptiveNames: boolean,
|
||||
precision: number = 2
|
||||
): string {
|
||||
try {
|
||||
const convertedValue = convertValue(value, fromUnit, toUnit);
|
||||
const displayUnit = getDisplayUnit(
|
||||
convertedValue,
|
||||
toUnit,
|
||||
useDescriptiveNames
|
||||
);
|
||||
return `${convertedValue.toFixed(precision)} ${displayUnit}`;
|
||||
} catch (e) {
|
||||
console.error("Conversion error:", e);
|
||||
return `[${value}${fromUnit}|${toUnit}]`; // Return original format on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expression to match unit conversion syntax
|
||||
*/
|
||||
export const CONVERSION_REGEX =
|
||||
/\[([\d.]+)([a-zA-Z0-9\-/]+)\|([a-zA-Z0-9\-/]+)\]/g;
|
||||
|
|
@ -8,4 +8,4 @@
|
|||
.measure-from-container .setting-item {
|
||||
border-top: none;
|
||||
padding: 0.75em 0;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue