mirror of
https://github.com/youfoundjk/TeXcore.git
synced 2026-07-22 07:33:31 +00:00
refactor: reorganize src into modular structure and increase test coverage
This commit is contained in:
parent
0f00ee5f90
commit
b6392a5523
63 changed files with 8973 additions and 6485 deletions
|
|
@ -1,47 +1,47 @@
|
|||
labels: []
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "⚠️ Please check that this feature request hasn't been suggested before."
|
||||
options:
|
||||
- label: "I searched previous [Ideas in Discussions](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas) and didn't find any similar feature requests."
|
||||
required: true
|
||||
- label: "I searched previous [Issues](https://github.com/YouFoundJK/ObsiTeXcore/issues) and didn't find any similar feature requests."
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "⚠️ Please check that this feature request hasn't been suggested before."
|
||||
options:
|
||||
- label: "I searched previous [Ideas in Discussions](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas) and didn't find any similar feature requests."
|
||||
required: true
|
||||
attributes:
|
||||
label: "🔖 Feature description"
|
||||
description: "A clear and concise description of what the feature request is."
|
||||
placeholder: "You should add ..."
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
validations:
|
||||
- label: "I searched previous [Issues](https://github.com/YouFoundJK/ObsiTeXcore/issues) and didn't find any similar feature requests."
|
||||
required: true
|
||||
attributes:
|
||||
label: "✔️ Solution"
|
||||
description: "A clear and concise description of what you want to happen, and why."
|
||||
placeholder: "In my use-case, ..."
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "❓ Alternatives"
|
||||
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||
placeholder: "I have considered ..."
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: '🔖 Feature description'
|
||||
description: 'A clear and concise description of what the feature request is.'
|
||||
placeholder: 'You should add ...'
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "📝 Additional Context"
|
||||
description: "Add any other context or screenshots about the feature request here."
|
||||
placeholder: "..."
|
||||
- type: textarea
|
||||
id: solution
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: '✔️ Solution'
|
||||
description: 'A clear and concise description of what you want to happen, and why.'
|
||||
placeholder: 'In my use-case, ...'
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: '❓ Alternatives'
|
||||
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||
placeholder: 'I have considered ...'
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: '📝 Additional Context'
|
||||
description: 'Add any other context or screenshots about the feature request here.'
|
||||
placeholder: '...'
|
||||
|
|
|
|||
198
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
198
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
|
@ -1,114 +1,114 @@
|
|||
name: Bug Report
|
||||
description: Create a bug report to help us improve ObsiTeXcore
|
||||
labels: ["bug"]
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Before you start
|
||||
Please **make sure you are on the latest version of ObsiTeXcore.**
|
||||
If you encountered the issue after you installed, updated, or reloaded the plugin, **please try restarting Obsidian before reporting the bug**.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Before you start
|
||||
Please **make sure you are on the latest version of ObsiTeXcore.**
|
||||
If you encountered the issue after you installed, updated, or reloaded the plugin, **please try restarting Obsidian before reporting the bug**.
|
||||
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "Please check that this issue hasn't been reported before."
|
||||
description: "The **Label filters** may help make your search more focused."
|
||||
options:
|
||||
- label: "I searched previous [Bug Reports](https://github.com/YouFoundJK/ObsiTeXcore/issues?q=state%3Aopen%20label%3Abug) and didn't find any similar reports."
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Tell us what **should** happen.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
id: no-duplicate-issues
|
||||
attributes:
|
||||
label: "Please check that this issue hasn't been reported before."
|
||||
description: 'The **Label filters** may help make your search more focused.'
|
||||
options:
|
||||
- label: "I searched previous [Bug Reports](https://github.com/YouFoundJK/ObsiTeXcore/issues?q=state%3Aopen%20label%3Abug) and didn't find any similar reports."
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Current behaviour
|
||||
description: |
|
||||
Tell us what happens instead of the expected behavior.
|
||||
Adding screenshots really helps.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Tell us what **should** happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: console
|
||||
attributes:
|
||||
label: JavaScript console output
|
||||
description: |
|
||||
Go to `View->Toggle Developer Tools` and open up the JavaScript console, reproduce your bug, and copy/paste any logs you see there.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Current behaviour
|
||||
description: |
|
||||
Tell us what happens instead of the expected behavior.
|
||||
Adding screenshots really helps.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Which exact steps can we take to reproduce the issue? The more detail you provide, the easier it will be to narrow down and fix the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: console
|
||||
attributes:
|
||||
label: JavaScript console output
|
||||
description: |
|
||||
Go to `View->Toggle Developer Tools` and open up the JavaScript console, reproduce your bug, and copy/paste any logs you see there.
|
||||
|
||||
- type: textarea
|
||||
id: markdown-sample
|
||||
attributes:
|
||||
label: Markdown/LaTeX snippet sample
|
||||
description: |
|
||||
Please provide the exact Markdown / LaTeX content or note configuration that triggers the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Which exact steps can we take to reproduce the issue? The more detail you provide, the easier it will be to narrow down and fix the bug.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: operating-systems
|
||||
attributes:
|
||||
label: Which Operating Systems are you using?
|
||||
description: You may select more than one.
|
||||
options:
|
||||
- label: macOS
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: Android
|
||||
- label: iOS
|
||||
- type: textarea
|
||||
id: markdown-sample
|
||||
attributes:
|
||||
label: Markdown/LaTeX snippet sample
|
||||
description: |
|
||||
Please provide the exact Markdown / LaTeX content or note configuration that triggers the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: obsidian-version
|
||||
attributes:
|
||||
label: Obsidian Version
|
||||
description: Which Obsidian version are you using?
|
||||
placeholder: "Enter Obsidian Version here."
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: operating-systems
|
||||
attributes:
|
||||
label: Which Operating Systems are you using?
|
||||
description: You may select more than one.
|
||||
options:
|
||||
- label: macOS
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: Android
|
||||
- label: iOS
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Plugin Version
|
||||
description: Which ObsiTeXcore version are you using?
|
||||
placeholder: "Enter plugin version here."
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: obsidian-version
|
||||
attributes:
|
||||
label: Obsidian Version
|
||||
description: Which Obsidian version are you using?
|
||||
placeholder: 'Enter Obsidian Version here.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: other-plugins-disabled
|
||||
attributes:
|
||||
label: Checks
|
||||
description: Please confirm
|
||||
options:
|
||||
- label: I have tried it with all other plugins disabled and the error still occurs.
|
||||
required: false
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Plugin Version
|
||||
description: Which ObsiTeXcore version are you using?
|
||||
placeholder: 'Enter plugin version here.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: possible-solution
|
||||
attributes:
|
||||
label: Possible solution
|
||||
description: |
|
||||
Not obligatory, but please suggest a fix or reason for the bug, if you have an idea.
|
||||
- type: checkboxes
|
||||
id: other-plugins-disabled
|
||||
attributes:
|
||||
label: Checks
|
||||
description: Please confirm
|
||||
options:
|
||||
- label: I have tried it with all other plugins disabled and the error still occurs.
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: comments
|
||||
attributes:
|
||||
label: Comments
|
||||
description: Anything you'd like to share that didn't fit into an above box.
|
||||
- type: textarea
|
||||
id: possible-solution
|
||||
attributes:
|
||||
label: Possible solution
|
||||
description: |
|
||||
Not obligatory, but please suggest a fix or reason for the bug, if you have an idea.
|
||||
|
||||
- type: textarea
|
||||
id: comments
|
||||
attributes:
|
||||
label: Comments
|
||||
description: Anything you'd like to share that didn't fit into an above box.
|
||||
|
|
|
|||
24
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
24
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
|
|
@ -2,15 +2,15 @@ name: Feature Request
|
|||
description: Please submit feature requests as a Discussion.
|
||||
labels: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Feature Requests have moved to Discussions
|
||||
Please [submit feature requests as Discussions](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas) so that they can be voted on and prioritized.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Submit on Discussions
|
||||
description: Please submit your feature request as a Discussion.
|
||||
options:
|
||||
- label: I will submit my feature request as a [Discussion](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas).
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Feature Requests have moved to Discussions
|
||||
Please [submit feature requests as Discussions](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas) so that they can be voted on and prioritized.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Submit on Discussions
|
||||
description: Please submit your feature request as a Discussion.
|
||||
options:
|
||||
- label: I will submit my feature request as a [Discussion](https://github.com/YouFoundJK/ObsiTeXcore/discussions/categories/ideas).
|
||||
required: true
|
||||
|
|
|
|||
52
.github/workflows/check.yml
vendored
52
.github/workflows/check.yml
vendored
|
|
@ -1,29 +1,29 @@
|
|||
name: Check
|
||||
on:
|
||||
- push
|
||||
- push
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.x"
|
||||
- uses: pnpm/action-setup@v6
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run compile
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.x"
|
||||
- uses: pnpm/action-setup@v6
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm run test-update
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- uses: pnpm/action-setup@v6
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run compile
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- uses: pnpm/action-setup@v6
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm run test-update
|
||||
|
|
|
|||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
|
|
@ -55,4 +55,4 @@ jobs:
|
|||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./site
|
||||
force_orphan: true
|
||||
force_orphan: true
|
||||
|
|
|
|||
90
.github/workflows/draft-release-attest.yml
vendored
90
.github/workflows/draft-release-attest.yml
vendored
|
|
@ -1,60 +1,60 @@
|
|||
name: Draft Release With Attestation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
draft-release:
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
draft-release:
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.x"
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build release artifacts
|
||||
run: pnpm run prod:release
|
||||
- name: Build release artifacts
|
||||
run: pnpm run prod:release
|
||||
|
||||
- name: Attest artifacts
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
release-artifacts/main.js
|
||||
release-artifacts/styles.css
|
||||
release-artifacts/manifest.json
|
||||
- name: Attest artifacts
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
release-artifacts/main.js
|
||||
release-artifacts/styles.css
|
||||
release-artifacts/manifest.json
|
||||
|
||||
- name: Compute draft release metadata
|
||||
id: metadata
|
||||
run: |
|
||||
echo "tag_name=draft-main-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_name=Draft Main Build #${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute draft release metadata
|
||||
id: metadata
|
||||
run: |
|
||||
echo "tag_name=draft-main-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_name=Draft Main Build #${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create draft release and upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.metadata.outputs.tag_name }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: ${{ steps.metadata.outputs.release_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: |
|
||||
release-artifacts/main.js
|
||||
release-artifacts/styles.css
|
||||
release-artifacts/manifest.json
|
||||
- name: Create draft release and upload assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.metadata.outputs.tag_name }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: ${{ steps.metadata.outputs.release_name }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
files: |
|
||||
release-artifacts/main.js
|
||||
release-artifacts/styles.css
|
||||
release-artifacts/manifest.json
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Release Obsidian plugin
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.x"
|
||||
node-version: '22.x'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Ryota Ushio
|
||||
Copyright (c) 2026 Jovi Koikkara
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -1,6 +1,11 @@
|
|||
<div align="right">
|
||||
<img src="https://img.shields.io/github/downloads/YouFoundJK/ObsiTeXcore/total?label=Downloads" alt="Downloads" />
|
||||
<a href="https://youfoundjk.github.io/ObsiTeXcore/"><img src="https://img.shields.io/badge/Version-v_0.0.1-blue" alt="Version" /></a>
|
||||
</div>
|
||||
|
||||
# ObsiTeXcore for Obsidian
|
||||
|
||||
> **ObsiTeXcore** is a minimalistic [Obsidian.md](https://obsidian.md/) plugin for automatic equation numbering and referencing. It is a streamlined fork of the original [LaTeX-like Theorem & Equation Referencer](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/) by Ryota Ushio.
|
||||
A minimalistic [Obsidian.md](https://obsidian.md/) plugin for automatic equation numbering and referencing.
|
||||
|
||||
📚 **[Full Documentation](https://youfoundjk.github.io/ObsiTeXcore/)**
|
||||
|
||||
|
|
@ -106,10 +111,6 @@ Use the "Fix callout equations" command to repair broken indentation.
|
|||
|
||||
See the [Settings Reference](https://youfoundjk.github.io/ObsiTeXcore/configuration/settings/) for all configuration options.
|
||||
|
||||
## Credits
|
||||
|
||||
- Original plugin by [Ryota Ushio](https://github.com/RyotaUshio)
|
||||
- PDF export based on [better-export-pdf](https://github.com/l1xnan/obsidian-better-export-pdf)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
module.exports = "# {{MONTH}} {{TOTAL_CALENDARS}} {{LIFETIME_OPS}} {{PREV_MONTH_OPS}} {{PREV_MONTH_CREATED}} {{LIFETIME_STREAK}} {{LIFETIME_TZ}} {{CALENDARS_TABLE}} {{NLP_CREATED}} {{RECURRING_CREATED}} {{WORKSPACES_COUNT}} {{NIGHT_OWL_OPS}} {{EARLY_BIRD_OPS}} {{WEEKEND_OPS}} {{JSON_PAYLOAD}}";
|
||||
module.exports =
|
||||
'# {{MONTH}} {{TOTAL_CALENDARS}} {{LIFETIME_OPS}} {{PREV_MONTH_OPS}} {{PREV_MONTH_CREATED}} {{LIFETIME_STREAK}} {{LIFETIME_TZ}} {{CALENDARS_TABLE}} {{NLP_CREATED}} {{RECURRING_CREATED}} {{WORKSPACES_COUNT}} {{NIGHT_OWL_OPS}} {{EARLY_BIRD_OPS}} {{WEEKEND_OPS}} {{JSON_PAYLOAD}}';
|
||||
|
|
|
|||
|
|
@ -1,168 +1,175 @@
|
|||
import { StateField } from '@codemirror/state';
|
||||
|
||||
const toForwardSlashes = (value: string): string => value.replace(/\\/g, "/");
|
||||
const toForwardSlashes = (value: string): string => value.replace(/\\/g, '/');
|
||||
const joinPath = (...parts: string[]): string => {
|
||||
const normalizedParts = parts
|
||||
.map(toForwardSlashes)
|
||||
.filter((part) => part !== "" && part !== ".");
|
||||
const joined = normalizedParts.join("/").replace(/\/+/g, "/");
|
||||
return joined === "" ? "." : joined;
|
||||
const normalizedParts = parts.map(toForwardSlashes).filter(part => part !== '' && part !== '.');
|
||||
const joined = normalizedParts.join('/').replace(/\/+/g, '/');
|
||||
return joined === '' ? '.' : joined;
|
||||
};
|
||||
const extname = (fileName: string): string => {
|
||||
const base = toForwardSlashes(fileName).split("/").pop() || "";
|
||||
const dotIndex = base.lastIndexOf(".");
|
||||
return dotIndex >= 0 ? base.slice(dotIndex) : "";
|
||||
const base = toForwardSlashes(fileName).split('/').pop() || '';
|
||||
const dotIndex = base.lastIndexOf('.');
|
||||
return dotIndex >= 0 ? base.slice(dotIndex) : '';
|
||||
};
|
||||
const basename = (fileName: string, ext?: string): string => {
|
||||
const base = toForwardSlashes(fileName).split("/").pop() || "";
|
||||
if (ext && base.endsWith(ext)) {
|
||||
return base.slice(0, -ext.length);
|
||||
}
|
||||
return base;
|
||||
const base = toForwardSlashes(fileName).split('/').pop() || '';
|
||||
if (ext && base.endsWith(ext)) {
|
||||
return base.slice(0, -ext.length);
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
/** Basic obsidian abstraction for any file or folder in a vault. */
|
||||
export abstract class TAbstractFile {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
get path(): string {
|
||||
const parentPath = this.parent?.path || "";
|
||||
const path = joinPath(parentPath, this.name);
|
||||
if (path.startsWith("/") && path.length > 1) {
|
||||
return path.slice(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
get path(): string {
|
||||
const parentPath = this.parent?.path || '';
|
||||
const path = joinPath(parentPath, this.name);
|
||||
if (path.startsWith('/') && path.length > 1) {
|
||||
return path.slice(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
name: string = "";
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
parent: TFolder | null = null;
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
name: string = '';
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
parent: TFolder | null = null;
|
||||
}
|
||||
|
||||
/** A regular file in the vault. */
|
||||
export class TFile extends TAbstractFile {
|
||||
get basename(): string {
|
||||
return basename(this.name, extname(this.name));
|
||||
}
|
||||
get basename(): string {
|
||||
return basename(this.name, extname(this.name));
|
||||
}
|
||||
|
||||
get extension(): string {
|
||||
const ext = extname(this.name);
|
||||
if (ext.startsWith(".")) {
|
||||
return ext.slice(1);
|
||||
} else {
|
||||
return ext;
|
||||
}
|
||||
get extension(): string {
|
||||
const ext = extname(this.name);
|
||||
if (ext.startsWith('.')) {
|
||||
return ext.slice(1);
|
||||
} else {
|
||||
return ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A folder in the vault. */
|
||||
export class TFolder extends TAbstractFile {
|
||||
children: TAbstractFile[] = [];
|
||||
children: TAbstractFile[] = [];
|
||||
|
||||
isRoot(): boolean {
|
||||
return this.path === "/";
|
||||
}
|
||||
isRoot(): boolean {
|
||||
return this.path === '/';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseYaml(yaml: string): Record<string, string> | null {
|
||||
const [k, ...v] = yaml.split(":");
|
||||
if (!k || !v) {
|
||||
return null;
|
||||
}
|
||||
return Object.fromEntries([[k.trim(), v.join(":").trim()]]);
|
||||
const [k, ...v] = yaml.split(':');
|
||||
if (!k || !v) {
|
||||
return null;
|
||||
}
|
||||
return Object.fromEntries([[k.trim(), v.join(':').trim()]]);
|
||||
}
|
||||
|
||||
export class Notice {
|
||||
static notices: string[] = [];
|
||||
static notices: string[] = [];
|
||||
|
||||
constructor(message: string) {
|
||||
Notice.notices.push(message);
|
||||
}
|
||||
constructor(message: string) {
|
||||
Notice.notices.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePath(path: string): string {
|
||||
return toForwardSlashes(path).replace(/\/+/g, "/");
|
||||
return toForwardSlashes(path).replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
export async function requestUrl(_url: string): Promise<{ text: string }> {
|
||||
await Promise.resolve();
|
||||
return { text: "{}" };
|
||||
await Promise.resolve();
|
||||
return { text: '{}' };
|
||||
}
|
||||
|
||||
export const getLanguage = jest.fn().mockReturnValue("en");
|
||||
export const getLanguage = jest.fn().mockReturnValue('en');
|
||||
|
||||
export const editorInfoField = StateField.define<unknown>({
|
||||
create: () => null,
|
||||
update: (val: unknown) => val
|
||||
create: () => null,
|
||||
update: (val: unknown) => val
|
||||
});
|
||||
|
||||
export class Component {}
|
||||
export class MarkdownRenderChild extends Component {}
|
||||
export class Modal {
|
||||
contentEl: HTMLElement;
|
||||
constructor(public app: unknown) {
|
||||
this.contentEl = document.createElement("div");
|
||||
}
|
||||
open() {}
|
||||
close() {}
|
||||
onOpen() {}
|
||||
onClose() {}
|
||||
contentEl: HTMLElement;
|
||||
constructor(public app: unknown) {
|
||||
this.contentEl = document.createElement('div');
|
||||
}
|
||||
open() {}
|
||||
close() {}
|
||||
onOpen() {}
|
||||
onClose() {}
|
||||
}
|
||||
export class App {}
|
||||
|
||||
export class PluginSettingTab {
|
||||
containerEl: HTMLElement;
|
||||
constructor(public app: unknown, public plugin: unknown) {
|
||||
this.containerEl = document.createElement("div");
|
||||
}
|
||||
containerEl: HTMLElement;
|
||||
constructor(
|
||||
public app: unknown,
|
||||
public plugin: unknown
|
||||
) {
|
||||
this.containerEl = document.createElement('div');
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceLeaf {}
|
||||
export class ItemView {
|
||||
constructor(public leaf: unknown) {}
|
||||
constructor(public leaf: unknown) {}
|
||||
}
|
||||
|
||||
export class DropdownComponent {
|
||||
|
||||
value: string = "";
|
||||
addOptions(options: Record<string, string>): this {
|
||||
return this;
|
||||
}
|
||||
getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
setValue(value: string): this {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
value: string = '';
|
||||
addOptions(options: Record<string, string>): this {
|
||||
return this;
|
||||
}
|
||||
getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
setValue(value: string): this {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Setting {
|
||||
constructor(public containerEl: HTMLElement) {}
|
||||
setName(name: string): this { return this; }
|
||||
setDesc(desc: unknown): this { return this; }
|
||||
setHeading(): this { return this; }
|
||||
addDropdown(cb: (d: DropdownComponent) => unknown): this {
|
||||
cb(new DropdownComponent());
|
||||
return this;
|
||||
}
|
||||
addExtraButton(cb: (b: unknown) => unknown): this {
|
||||
const button = {
|
||||
setTooltip: () => button,
|
||||
setIcon: () => button,
|
||||
onClick: (onClickCb: unknown) => { return button; }
|
||||
};
|
||||
cb(button);
|
||||
return this;
|
||||
}
|
||||
constructor(public containerEl: HTMLElement) {}
|
||||
setName(name: string): this {
|
||||
return this;
|
||||
}
|
||||
setDesc(desc: unknown): this {
|
||||
return this;
|
||||
}
|
||||
setHeading(): this {
|
||||
return this;
|
||||
}
|
||||
addDropdown(cb: (d: DropdownComponent) => unknown): this {
|
||||
cb(new DropdownComponent());
|
||||
return this;
|
||||
}
|
||||
addExtraButton(cb: (b: unknown) => unknown): this {
|
||||
const button = {
|
||||
setTooltip: () => button,
|
||||
setIcon: () => button,
|
||||
onClick: (onClickCb: unknown) => {
|
||||
return button;
|
||||
}
|
||||
};
|
||||
cb(button);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function setIcon(el: HTMLElement, iconId: string): void {}
|
||||
export const activeDocument = typeof document !== 'undefined' ? document : null;
|
||||
|
|
|
|||
|
|
@ -1,248 +1,253 @@
|
|||
// Global URL Interceptor to clean paths before sending to Umami
|
||||
window.cleanUmamiUrl = function (type, payload) {
|
||||
if (payload && payload.url) {
|
||||
// Strip the repository name from the URL prefix for cleaner dashboard visibility
|
||||
payload.url = payload.url.replace('/ObsiTeXcore', '') || '/';
|
||||
}
|
||||
return payload;
|
||||
if (payload && payload.url) {
|
||||
// Strip the repository name from the URL prefix for cleaner dashboard visibility
|
||||
payload.url = payload.url.replace('/ObsiTeXcore', '') || '/';
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
// 1. Load Umami Script Dynamically (Let native auto-investigate do the heavy lifting)
|
||||
(function () {
|
||||
var el = document.createElement('script');
|
||||
el.setAttribute('src', 'https://injest.destyleer.in.net/assets/js/theme-utils.js');
|
||||
el.setAttribute('data-website-id', 'b18306c0-3f20-4089-baaf-bcb771cb9cd2');
|
||||
el.setAttribute('data-host-url', 'https://injest.destyleer.in.net');
|
||||
var el = document.createElement('script');
|
||||
el.setAttribute('src', 'https://injest.destyleer.in.net/assets/js/theme-utils.js');
|
||||
el.setAttribute('data-website-id', '7dd635ed-ee02-4b53-9aba-eefcf6f3cad1');
|
||||
el.setAttribute('data-host-url', 'https://injest.destyleer.in.net');
|
||||
|
||||
// Attach the interceptor to Umami's script initialization
|
||||
el.setAttribute('data-before-send', 'cleanUmamiUrl');
|
||||
// Attach the interceptor to Umami's script initialization
|
||||
el.setAttribute('data-before-send', 'cleanUmamiUrl');
|
||||
|
||||
// Core Web Vitals natively
|
||||
el.setAttribute('data-performance', 'true');
|
||||
// Core Web Vitals natively
|
||||
el.setAttribute('data-performance', 'true');
|
||||
|
||||
// Feature Extension: Sync session traits immediately upon script load completion
|
||||
el.onload = function () {
|
||||
syncUmamiSessionProperties();
|
||||
};
|
||||
document.head.appendChild(el);
|
||||
// Feature Extension: Sync session traits immediately upon script load completion
|
||||
el.onload = function () {
|
||||
syncUmamiSessionProperties();
|
||||
};
|
||||
document.head.appendChild(el);
|
||||
})();
|
||||
|
||||
// Global Helper for Custom Umami Event investigateing
|
||||
function investigateUmamiEvent(eventName, eventData) {
|
||||
if (window.umami && typeof window.umami.track === 'function') {
|
||||
|
||||
// Ensure the repository subdirectory name is stripped from custom events too
|
||||
if (eventData && eventData.page) {
|
||||
eventData.page = eventData.page.replace('/ObsiTeXcore', '') || '/';
|
||||
}
|
||||
|
||||
window.umami.track(eventName, eventData);
|
||||
if (window.umami && typeof window.umami.track === 'function') {
|
||||
// Ensure the repository subdirectory name is stripped from custom events too
|
||||
if (eventData && eventData.page) {
|
||||
eventData.page = eventData.page.replace('/ObsiTeXcore', '') || '/';
|
||||
}
|
||||
|
||||
window.umami.track(eventName, eventData);
|
||||
}
|
||||
}
|
||||
|
||||
// Anonymous Session Properties Identification
|
||||
function syncUmamiSessionProperties() {
|
||||
if (window.umami && typeof window.umami.identify === 'function') {
|
||||
var activeScheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
||||
if (window.umami && typeof window.umami.identify === 'function') {
|
||||
var activeScheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
||||
|
||||
window.umami.identify({
|
||||
theme_preference: activeScheme,
|
||||
screen_tier: window.innerWidth > 1200 ? 'desktop_wide' : (window.innerWidth > 768 ? 'tablet' : 'mobile'),
|
||||
device_orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
|
||||
browser_language: navigator.language || 'unknown'
|
||||
});
|
||||
}
|
||||
window.umami.identify({
|
||||
theme_preference: activeScheme,
|
||||
screen_tier:
|
||||
window.innerWidth > 1200 ? 'desktop_wide' : window.innerWidth > 768 ? 'tablet' : 'mobile',
|
||||
device_orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
|
||||
browser_language: navigator.language || 'unknown'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// investigate Document Printing / PDF Exports
|
||||
window.addEventListener('beforeprint', function () {
|
||||
investigateUmamiEvent('exported_pdf', { page: window.location.pathname });
|
||||
investigateUmamiEvent('exported_pdf', { page: window.location.pathname });
|
||||
});
|
||||
|
||||
// Move script timers and scroll components to global scope to prevent event listener leakage
|
||||
let activeTimers = [];
|
||||
const clearActiveTimers = () => {
|
||||
activeTimers.forEach(clearTimeout);
|
||||
activeTimers = [];
|
||||
activeTimers.forEach(clearTimeout);
|
||||
activeTimers = [];
|
||||
};
|
||||
|
||||
var scrollTriggered = false;
|
||||
var scrollHandler = function () {
|
||||
if (!scrollTriggered) {
|
||||
var h = document.documentElement,
|
||||
b = document.body,
|
||||
st = 'scrollTop',
|
||||
sh = 'scrollHeight';
|
||||
var percent = (h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight) * 100;
|
||||
if (percent >= 90) {
|
||||
scrollTriggered = true;
|
||||
investigateUmamiEvent('scrolled_90', { page: window.location.pathname });
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
}
|
||||
if (!scrollTriggered) {
|
||||
var h = document.documentElement,
|
||||
b = document.body,
|
||||
st = 'scrollTop',
|
||||
sh = 'scrollHeight';
|
||||
var percent = ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100;
|
||||
if (percent >= 90) {
|
||||
scrollTriggered = true;
|
||||
investigateUmamiEvent('scrolled_90', { page: window.location.pathname });
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into MkDocs Material Single Page Application lifecycle
|
||||
document$.subscribe(function () {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
// SAFE CLEANUP: Wipe out investigateing elements from the previous page cycle
|
||||
clearActiveTimers();
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
scrollTriggered = false;
|
||||
window.addEventListener('scroll', scrollHandler);
|
||||
|
||||
// SAFE CLEANUP: Wipe out investigateing elements from the previous page cycle
|
||||
clearActiveTimers();
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
scrollTriggered = false;
|
||||
window.addEventListener('scroll', scrollHandler);
|
||||
// Sync variables on SPA shifts to catch changes in browser traits
|
||||
syncUmamiSessionProperties();
|
||||
|
||||
// Sync variables on SPA shifts to catch changes in browser traits
|
||||
syncUmamiSessionProperties();
|
||||
|
||||
// --- 404 Error investigateer ---
|
||||
// MkDocs assigns a specific title or attribute to its 404 pages.
|
||||
if (document.title.includes("404") || document.querySelector('.md-main h1')?.innerText.includes("404")) {
|
||||
investigateUmamiEvent('404_error', {
|
||||
broken_url: currentPath,
|
||||
came_from: document.referrer || 'Direct Link / Bookmark'
|
||||
});
|
||||
}
|
||||
|
||||
// 3. investigate & REVEAL Feedback Widget (Happy/Sad)
|
||||
var feedback = document.forms.feedback;
|
||||
if (typeof feedback !== "undefined" && feedback !== null) {
|
||||
feedback.hidden = false;
|
||||
|
||||
feedback.addEventListener("submit", function (ev) {
|
||||
ev.preventDefault();
|
||||
if (feedback.firstElementChild) {
|
||||
feedback.firstElementChild.disabled = true;
|
||||
}
|
||||
|
||||
var data = ev.submitter.getAttribute("data-md-value");
|
||||
var rating = data === "1" ? "Happy" : "Sad";
|
||||
|
||||
investigateUmamiEvent('docs_feedback', {
|
||||
rating: rating,
|
||||
page: currentPath
|
||||
});
|
||||
|
||||
var note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']");
|
||||
if (note) {
|
||||
note.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. investigate "Copy to Clipboard"
|
||||
var copyButtons = document.querySelectorAll('.md-clipboard');
|
||||
copyButtons.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var codeBlock = btn.closest('.highlight');
|
||||
var language = codeBlock ? codeBlock.className.replace('highlight', '').trim() : 'unknown';
|
||||
|
||||
investigateUmamiEvent('copied_code', {
|
||||
language: language,
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
// --- 404 Error investigateer ---
|
||||
// MkDocs assigns a specific title or attribute to its 404 pages.
|
||||
if (
|
||||
document.title.includes('404') ||
|
||||
document.querySelector('.md-main h1')?.innerText.includes('404')
|
||||
) {
|
||||
investigateUmamiEvent('404_error', {
|
||||
broken_url: currentPath,
|
||||
came_from: document.referrer || 'Direct Link / Bookmark'
|
||||
});
|
||||
}
|
||||
|
||||
// 5. investigate Dark/Light Mode Toggles
|
||||
var colorToggles = document.querySelectorAll('[data-md-color-scheme]');
|
||||
colorToggles.forEach(function (toggle) {
|
||||
toggle.addEventListener('change', function (ev) {
|
||||
investigateUmamiEvent('toggled_theme', {
|
||||
theme: ev.target.value
|
||||
});
|
||||
setTimeout(syncUmamiSessionProperties, 100);
|
||||
});
|
||||
// 3. investigate & REVEAL Feedback Widget (Happy/Sad)
|
||||
var feedback = document.forms.feedback;
|
||||
if (typeof feedback !== 'undefined' && feedback !== null) {
|
||||
feedback.hidden = false;
|
||||
|
||||
feedback.addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
if (feedback.firstElementChild) {
|
||||
feedback.firstElementChild.disabled = true;
|
||||
}
|
||||
|
||||
var data = ev.submitter.getAttribute('data-md-value');
|
||||
var rating = data === '1' ? 'Happy' : 'Sad';
|
||||
|
||||
investigateUmamiEvent('docs_feedback', {
|
||||
rating: rating,
|
||||
page: currentPath
|
||||
});
|
||||
|
||||
var note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']");
|
||||
if (note) {
|
||||
note.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. investigate Content Tab Switching
|
||||
var tabs = document.querySelectorAll('.tabbed-labels > label');
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
investigateUmamiEvent('switched_tab', {
|
||||
tab_name: tab.innerText.trim(),
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
// 4. investigate "Copy to Clipboard"
|
||||
var copyButtons = document.querySelectorAll('.md-clipboard');
|
||||
copyButtons.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var codeBlock = btn.closest('.highlight');
|
||||
var language = codeBlock ? codeBlock.className.replace('highlight', '').trim() : 'unknown';
|
||||
|
||||
investigateUmamiEvent('copied_code', {
|
||||
language: language,
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 7. investigate Right-Sidebar Table of Contents (Anchor Link investigateing)
|
||||
var tocLinks = document.querySelectorAll('.md-nav__link[href^="#"]');
|
||||
tocLinks.forEach(function (link) {
|
||||
// 5. investigate Dark/Light Mode Toggles
|
||||
var colorToggles = document.querySelectorAll('[data-md-color-scheme]');
|
||||
colorToggles.forEach(function (toggle) {
|
||||
toggle.addEventListener('change', function (ev) {
|
||||
investigateUmamiEvent('toggled_theme', {
|
||||
theme: ev.target.value
|
||||
});
|
||||
setTimeout(syncUmamiSessionProperties, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 6. investigate Content Tab Switching
|
||||
var tabs = document.querySelectorAll('.tabbed-labels > label');
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
investigateUmamiEvent('switched_tab', {
|
||||
tab_name: tab.innerText.trim(),
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 7. investigate Right-Sidebar Table of Contents (Anchor Link investigateing)
|
||||
var tocLinks = document.querySelectorAll('.md-nav__link[href^="#"]');
|
||||
tocLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
investigateUmamiEvent('clicked_toc_anchor', {
|
||||
anchor: link.getAttribute('href'),
|
||||
anchor_text: link.innerText.trim(),
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 8. investigate Outbound External Links
|
||||
var allLinks = document.querySelectorAll('a[href^="http"]');
|
||||
allLinks.forEach(function (link) {
|
||||
try {
|
||||
var url = new URL(link.href);
|
||||
if (url.hostname !== 'injest.destyleer.in.net' && !url.hostname.includes('github.io')) {
|
||||
link.addEventListener('click', function () {
|
||||
investigateUmamiEvent('clicked_toc_anchor', {
|
||||
anchor: link.getAttribute('href'),
|
||||
anchor_text: link.innerText.trim(),
|
||||
page: currentPath
|
||||
});
|
||||
investigateUmamiEvent('outbound_exit', {
|
||||
destination: link.href,
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// 9. investigate Chronological Time-on-Page Milestones
|
||||
const setMilestone = (seconds, label) => {
|
||||
activeTimers.push(
|
||||
setTimeout(() => {
|
||||
investigateUmamiEvent('time_milestone', { duration: label, page: currentPath });
|
||||
}, seconds * 1000)
|
||||
);
|
||||
};
|
||||
|
||||
setMilestone(30, '30_seconds');
|
||||
setMilestone(120, '2_minutes');
|
||||
setMilestone(300, '5_minutes');
|
||||
|
||||
// 11. investigate Search Keywords
|
||||
var searchInput = document.querySelector('.md-search__input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('blur', function () {
|
||||
var query = searchInput.value.trim();
|
||||
if (query.length > 0) {
|
||||
setTimeout(function () {
|
||||
var meta = document.querySelector('.md-search__result-meta');
|
||||
var zeroResults = meta && (meta.innerText.includes('0') || meta.innerText.includes('No'));
|
||||
|
||||
investigateUmamiEvent('search', {
|
||||
keyword: query,
|
||||
status: zeroResults ? 'zero_results' : 'has_results',
|
||||
searched_from_page: currentPath
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 8. investigate Outbound External Links
|
||||
var allLinks = document.querySelectorAll('a[href^="http"]');
|
||||
allLinks.forEach(function (link) {
|
||||
try {
|
||||
var url = new URL(link.href);
|
||||
if (url.hostname !== 'injest.destyleer.in.net' && !url.hostname.includes('github.io')) {
|
||||
link.addEventListener('click', function () {
|
||||
investigateUmamiEvent('outbound_exit', {
|
||||
destination: link.href,
|
||||
page: currentPath
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) { }
|
||||
// --- NEW ADVANCED FEATURE: Search Result Click-Throughs ---
|
||||
var searchResultsContainer = document.querySelector('.md-search__output');
|
||||
if (searchResultsContainer) {
|
||||
searchResultsContainer.addEventListener('click', function (ev) {
|
||||
var clickedLink = ev.target.closest('.md-search-result__link');
|
||||
if (clickedLink) {
|
||||
var keywordEntered = document.querySelector('.md-search__input')?.value.trim() || 'unknown';
|
||||
var targetTitle =
|
||||
clickedLink.querySelector('h1, h2, h3, h4')?.innerText.trim() ||
|
||||
clickedLink.innerText.trim();
|
||||
|
||||
investigateUmamiEvent('search_clickthrough', {
|
||||
search_query: keywordEntered,
|
||||
clicked_document_title: targetTitle,
|
||||
clicked_target_url: clickedLink.getAttribute('href'),
|
||||
actioned_on_page: currentPath
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 9. investigate Chronological Time-on-Page Milestones
|
||||
const setMilestone = (seconds, label) => {
|
||||
activeTimers.push(setTimeout(() => {
|
||||
investigateUmamiEvent('time_milestone', { duration: label, page: currentPath });
|
||||
}, seconds * 1000));
|
||||
};
|
||||
|
||||
setMilestone(30, '30_seconds');
|
||||
setMilestone(120, '2_minutes');
|
||||
setMilestone(300, '5_minutes');
|
||||
|
||||
// 11. investigate Search Keywords
|
||||
var searchInput = document.querySelector('.md-search__input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('blur', function () {
|
||||
var query = searchInput.value.trim();
|
||||
if (query.length > 0) {
|
||||
setTimeout(function () {
|
||||
var meta = document.querySelector('.md-search__result-meta');
|
||||
var zeroResults = meta && (meta.innerText.includes('0') || meta.innerText.includes('No'));
|
||||
|
||||
investigateUmamiEvent('search', {
|
||||
keyword: query,
|
||||
status: zeroResults ? 'zero_results' : 'has_results',
|
||||
searched_from_page: currentPath
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- NEW ADVANCED FEATURE: Search Result Click-Throughs ---
|
||||
var searchResultsContainer = document.querySelector('.md-search__output');
|
||||
if (searchResultsContainer) {
|
||||
searchResultsContainer.addEventListener('click', function (ev) {
|
||||
var clickedLink = ev.target.closest('.md-search-result__link');
|
||||
if (clickedLink) {
|
||||
var keywordEntered = document.querySelector('.md-search__input')?.value.trim() || 'unknown';
|
||||
var targetTitle = clickedLink.querySelector('h1, h2, h3, h4')?.innerText.trim() || clickedLink.innerText.trim();
|
||||
|
||||
investigateUmamiEvent('search_clickthrough', {
|
||||
search_query: keywordEntered,
|
||||
clicked_document_title: targetTitle,
|
||||
clicked_target_url: clickedLink.getAttribute('href'),
|
||||
actioned_on_page: currentPath
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
function getSiteBaseUrl() {
|
||||
const candidates = [
|
||||
...document.querySelectorAll('link[href*="assets/stylesheets/"]'),
|
||||
...document.querySelectorAll('script[src*="assets/javascripts/"]'),
|
||||
...document.querySelectorAll('script[src*="assets/javascripts/"]')
|
||||
];
|
||||
|
||||
for (const el of candidates) {
|
||||
const raw = el.getAttribute("href") || el.getAttribute("src");
|
||||
const raw = el.getAttribute('href') || el.getAttribute('src');
|
||||
if (!raw) continue;
|
||||
|
||||
const absUrl = new URL(raw, window.location.href).href;
|
||||
const assetsIndex = absUrl.indexOf("/assets/");
|
||||
const assetsIndex = absUrl.indexOf('/assets/');
|
||||
if (assetsIndex !== -1) {
|
||||
return absUrl.slice(0, assetsIndex);
|
||||
}
|
||||
|
|
@ -25,26 +25,26 @@
|
|||
|
||||
function getScheme() {
|
||||
return (
|
||||
document.body?.getAttribute("data-md-color-scheme") ||
|
||||
document.documentElement?.getAttribute("data-md-color-scheme") ||
|
||||
"default"
|
||||
document.body?.getAttribute('data-md-color-scheme') ||
|
||||
document.documentElement?.getAttribute('data-md-color-scheme') ||
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
function applyThemeLogo() {
|
||||
const logoSrc =
|
||||
getScheme() === "slate" ? getLogoPath("icon-dark.png") : getLogoPath("icon-light.png");
|
||||
const logos = document.querySelectorAll(".md-header__button.md-logo img");
|
||||
getScheme() === 'slate' ? getLogoPath('icon-dark.png') : getLogoPath('icon-light.png');
|
||||
const logos = document.querySelectorAll('.md-header__button.md-logo img');
|
||||
|
||||
logos.forEach((logo) => {
|
||||
logo.setAttribute("src", logoSrc);
|
||||
logos.forEach(logo => {
|
||||
logo.setAttribute('src', logoSrc);
|
||||
});
|
||||
}
|
||||
|
||||
function observeThemeChanges() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "data-md-color-scheme") {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-md-color-scheme') {
|
||||
applyThemeLogo();
|
||||
break;
|
||||
}
|
||||
|
|
@ -54,13 +54,13 @@
|
|||
if (document.body) {
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-md-color-scheme"],
|
||||
attributeFilter: ['data-md-color-scheme']
|
||||
});
|
||||
}
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-md-color-scheme"],
|
||||
attributeFilter: ['data-md-color-scheme']
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -69,8 +69,8 @@
|
|||
observeThemeChanges();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
gap: 1.1rem 2.1rem;
|
||||
grid-template-columns: minmax(220px, 1.25fr) minmax(220px, 1fr) minmax(260px, 1.2fr);
|
||||
align-items: start;
|
||||
padding-top: 2.0rem;
|
||||
padding-bottom: 2.0rem;
|
||||
padding-left: 2.0rem;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.custom-footer__brand {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-sidebar .md-nav--primary .md-nav__item--nested > nav.md-nav > ul.md-nav__list > li.md-nav__item.fc-nav-index-entry {
|
||||
.md-sidebar
|
||||
.md-nav--primary
|
||||
.md-nav__item--nested
|
||||
> nav.md-nav
|
||||
> ul.md-nav__list
|
||||
> li.md-nav__item.fc-nav-index-entry {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@
|
|||
|
||||
.md-sidebar .md-nav__link {
|
||||
border-radius: var(--fc-nav-radius);
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
transition:
|
||||
background-color 120ms ease,
|
||||
color 120ms ease;
|
||||
}
|
||||
|
||||
.md-sidebar .md-nav__link:hover {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# ObsiTeXcore
|
||||
|
||||
A minimalistic Obsidian plugin for automatic equation numbering and referencing.
|
||||
A minimalistic Obsidian plugin for automatic equation numbering, referencing with support for PDE exports and .
|
||||
|
||||
|
||||
!!! info "Fork of Math Booster"
|
||||
This plugin is a streamlined fork of the original [LaTeX-like Theorem & Equation Referencer](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/) by Ryota Ushio. It focuses solely on equation referencing with improved performance and reliability.
|
||||
|
||||
## Key Features
|
||||
|
||||
|
|
@ -47,16 +46,6 @@ A minimalistic Obsidian plugin for automatic equation numbering and referencing.
|
|||
|
||||
</div>
|
||||
|
||||
## How It Differs from Original
|
||||
|
||||
| Feature | Original Math Booster | ObsiTeXcore |
|
||||
|---------|----------------------|---------------------------|
|
||||
| Scope | Vault-wide scanning | Active note only |
|
||||
| Theorem Support | ✅ Full support | ❌ Removed |
|
||||
| Block References | Obsidian's built-in | Custom `% id:` comments |
|
||||
| Dependencies | Quick Preview, Math Links | Built-in (no dependencies) |
|
||||
| Performance | Slower (vault scans) | Faster (single file) |
|
||||
|
||||
## Quick Example
|
||||
|
||||
Write your equation with an ID comment:
|
||||
|
|
@ -79,3 +68,8 @@ The equation will be automatically numbered as `(1)` and the link will display a
|
|||
## Getting Started
|
||||
|
||||
[:octicons-arrow-right-24: Installation Guide](getting-started.md)
|
||||
|
||||
## Special Thanks to
|
||||
- [LaTeX-like Theorem & Equation Referencer](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/) by Ryota Ushio on which this is inspired.
|
||||
- PDF export motivated by [better-export-pdf](https://github.com/l1xnan/obsidian-better-export-pdf)
|
||||
- [obsidian-tikzjax](https://github.com/artisticat1/obsidian-tikzjax) by [artisticat1](https://github.com/artisticat1)
|
||||
|
|
@ -13,11 +13,7 @@
|
|||
rel="noopener"
|
||||
aria-label="Support this project"
|
||||
>
|
||||
<img
|
||||
src="https://ko-fi.com/img/githubbutton_sm.svg"
|
||||
alt="Support me"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me" loading="lazy" />
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
|
@ -25,13 +21,19 @@
|
|||
<h3>Documentation</h3>
|
||||
<a href="https://youfoundjk.github.io/ObsiTeXcore/">Docs Home</a>
|
||||
<a href="https://youfoundjk.github.io/ObsiTeXcore/getting-started/">Getting Started</a>
|
||||
<a href="https://youfoundjk.github.io/ObsiTeXcore/configuration/settings/">Settings Reference</a>
|
||||
<a href="https://youfoundjk.github.io/ObsiTeXcore/configuration/settings/"
|
||||
>Settings Reference</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<nav class="custom-footer__column" aria-label="Project and support links">
|
||||
<h3>Project</h3>
|
||||
<a href="https://github.com/YouFoundJK/ObsiTeXcore" target="_blank" rel="noopener">GitHub Repository</a>
|
||||
<a href="https://github.com/YouFoundJK" target="_blank" rel="noopener">YouFoundJK on GitHub</a>
|
||||
<a href="https://github.com/YouFoundJK/ObsiTeXcore" target="_blank" rel="noopener"
|
||||
>GitHub Repository</a
|
||||
>
|
||||
<a href="https://github.com/YouFoundJK" target="_blank" rel="noopener"
|
||||
>YouFoundJK on GitHub</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "node:module";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
|
||||
import esbuildSvelte from "esbuild-svelte";
|
||||
import esbuild from 'esbuild';
|
||||
import process from 'process';
|
||||
import { builtinModules } from 'node:module';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import inlineWorkerPlugin from 'esbuild-plugin-inline-worker';
|
||||
import esbuildSvelte from 'esbuild-svelte';
|
||||
|
||||
const builtins = [...new Set([...builtinModules, ...builtinModules.map((moduleName) => `node:${moduleName}`)])];
|
||||
const builtins = [
|
||||
...new Set([...builtinModules, ...builtinModules.map(moduleName => `node:${moduleName}`)])
|
||||
];
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
|
@ -14,78 +16,87 @@ if you want to view the source, please visit the github repository of this plugi
|
|||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
const prod = process.argv[2] === 'production';
|
||||
|
||||
async function build() {
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
js: banner
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
entryPoints: ['src/main.ts'],
|
||||
bundle: true,
|
||||
plugins: [
|
||||
inlineWorkerPlugin(),
|
||||
esbuildSvelte({ compilerOptions: { css: "injected" } }),
|
||||
esbuildSvelte({ compilerOptions: { css: 'injected' } }),
|
||||
{
|
||||
name: 'copy-to-dev-vault-plugin',
|
||||
setup(build) {
|
||||
build.onEnd(() => {
|
||||
const rootDir = process.cwd();
|
||||
const devEnvDir = path.join(rootDir, "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations");
|
||||
|
||||
const devEnvDir = path.join(
|
||||
rootDir,
|
||||
'../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/ObsiTeXcore'
|
||||
);
|
||||
|
||||
// Ensure dev output directory exists
|
||||
try {
|
||||
if (!fs.existsSync(devEnvDir)) {
|
||||
fs.mkdirSync(devEnvDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("⚠️ Could not create output directory:", err.message);
|
||||
console.warn('⚠️ Could not create output directory:', err.message);
|
||||
}
|
||||
|
||||
// Copy build files
|
||||
try {
|
||||
if (fs.existsSync(path.join(rootDir, "main.js"))) {
|
||||
fs.copyFileSync(path.join(rootDir, "main.js"), path.join(devEnvDir, "main.js"));
|
||||
console.log(" | Copied main.js to dev vault");
|
||||
if (fs.existsSync(path.join(rootDir, 'main.js'))) {
|
||||
fs.copyFileSync(path.join(rootDir, 'main.js'), path.join(devEnvDir, 'main.js'));
|
||||
console.log(' | Copied main.js to dev vault');
|
||||
}
|
||||
if (fs.existsSync(path.join(rootDir, "styles.css"))) {
|
||||
fs.copyFileSync(path.join(rootDir, "styles.css"), path.join(devEnvDir, "styles.css"));
|
||||
console.log(" | Copied styles.css to dev vault");
|
||||
if (fs.existsSync(path.join(rootDir, 'styles.css'))) {
|
||||
fs.copyFileSync(
|
||||
path.join(rootDir, 'styles.css'),
|
||||
path.join(devEnvDir, 'styles.css')
|
||||
);
|
||||
console.log(' | Copied styles.css to dev vault');
|
||||
}
|
||||
if (fs.existsSync(path.join(rootDir, "manifest.json"))) {
|
||||
fs.copyFileSync(path.join(rootDir, "manifest.json"), path.join(devEnvDir, "manifest.json"));
|
||||
console.log(" | Copied manifest.json to dev vault");
|
||||
if (fs.existsSync(path.join(rootDir, 'manifest.json'))) {
|
||||
fs.copyFileSync(
|
||||
path.join(rootDir, 'manifest.json'),
|
||||
path.join(devEnvDir, 'manifest.json')
|
||||
);
|
||||
console.log(' | Copied manifest.json to dev vault');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("⚠️ Could not copy files to dev vault:", err.message);
|
||||
console.warn('⚠️ Could not copy files to dev vault:', err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
],
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
'obsidian',
|
||||
'electron',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/language',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/view',
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
...builtins
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2020",
|
||||
logLevel: "info",
|
||||
format: 'cjs',
|
||||
target: 'es2020',
|
||||
logLevel: 'info',
|
||||
minify: prod,
|
||||
sourcemap: prod ? false : "inline",
|
||||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
outfile: 'main.js'
|
||||
});
|
||||
|
||||
if (!prod) {
|
||||
|
|
@ -97,7 +108,7 @@ async function build() {
|
|||
}
|
||||
}
|
||||
|
||||
build().catch((err) => {
|
||||
build().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,122 +6,133 @@ import { fixupPluginRules } from '@eslint/compat';
|
|||
import globals from 'globals';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['node_modules/**', 'dist/**', 'build/**', 'main.js', '*.min.js', 'web/**', 'docs/**', 'coverage/**', 'obsidian-dev-vault/**', 'tools/**'],
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ['**/*.{js,cjs,mjs,ts,tsx}']
|
||||
},
|
||||
...tseslint.configs.recommended,
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.{test,spec}.{ts,tsx}', 'test_helpers/**/*.{ts,tsx}', '__mocks__/**/*.ts'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// Tests may legitimately use Node built-ins (e.g. reading fixture files).
|
||||
// Keep this rule enabled for `src/**` to avoid shipping Node-only imports in the plugin runtime.
|
||||
'import/no-nodejs-modules': 'off',
|
||||
'obsidianmd/no-nodejs-modules': 'off',
|
||||
// Tests run in Node and may use test harness globals instead of Obsidian's active document.
|
||||
'obsidianmd/prefer-active-doc': 'off',
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'describe',
|
||||
property: 'only',
|
||||
message: 'Do not commit describe.only()'
|
||||
},
|
||||
{
|
||||
object: 'it',
|
||||
property: 'only',
|
||||
message: 'Do not commit it.only()'
|
||||
},
|
||||
{
|
||||
object: 'test',
|
||||
property: 'only',
|
||||
message: 'Do not commit test.only()'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
ignores: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
React: 'readonly'
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': fixupPluginRules(reactHooks),
|
||||
obsidianmd: obsidianmd
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'no-console': 'off',
|
||||
'no-debugger': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: "NewExpression[callee.name='Notice']",
|
||||
message: 'Use showNotice(...) instead of new Notice(...).'
|
||||
}
|
||||
],
|
||||
// Upgrade the Obsidian trash rule from warn to error.
|
||||
'obsidianmd/prefer-file-manager-trash-file': 'error',
|
||||
|
||||
// Type assertions
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/prefer-as-const': 'warn',
|
||||
|
||||
// Code style
|
||||
eqeqeq: ['error', 'always'], // Require === and !== instead of == and !=
|
||||
'prefer-template': 'warn', // Use template literals instead of string concatenation
|
||||
'@typescript-eslint/array-type': ['warn', { default: 'array' }], // Prefer T[] over Array<T>
|
||||
'prefer-object-spread': 'warn', // Use {...obj} instead of Object.assign()
|
||||
curly: ['warn', 'multi-line'], // Require curly braces for multi-line blocks
|
||||
'no-else-return': 'warn', // Remove unnecessary else after return
|
||||
'obsidianmd/ui/sentence-case': [
|
||||
'warn',
|
||||
{
|
||||
acronyms: ['RRGGBB', 'RRGGBBAA']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/utils/eventActions.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off'
|
||||
}
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'build/**',
|
||||
'main.js',
|
||||
'*.min.js',
|
||||
'web/**',
|
||||
'docs/**',
|
||||
'coverage/**',
|
||||
'obsidian-dev-vault/**',
|
||||
'tools/**'
|
||||
],
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ['**/*.{js,cjs,mjs,ts,tsx}']
|
||||
},
|
||||
...tseslint.configs.recommended,
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.{test,spec}.{ts,tsx}', 'test_helpers/**/*.{ts,tsx}', '__mocks__/**/*.ts'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// Tests may legitimately use Node built-ins (e.g. reading fixture files).
|
||||
// Keep this rule enabled for `src/**` to avoid shipping Node-only imports in the plugin runtime.
|
||||
'import/no-nodejs-modules': 'off',
|
||||
'obsidianmd/no-nodejs-modules': 'off',
|
||||
// Tests run in Node and may use test harness globals instead of Obsidian's active document.
|
||||
'obsidianmd/prefer-active-doc': 'off',
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'describe',
|
||||
property: 'only',
|
||||
message: 'Do not commit describe.only()'
|
||||
},
|
||||
{
|
||||
object: 'it',
|
||||
property: 'only',
|
||||
message: 'Do not commit it.only()'
|
||||
},
|
||||
{
|
||||
object: 'test',
|
||||
property: 'only',
|
||||
message: 'Do not commit test.only()'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
ignores: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
React: 'readonly'
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': fixupPluginRules(reactHooks),
|
||||
obsidianmd: obsidianmd
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'no-console': 'off',
|
||||
'no-debugger': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: "NewExpression[callee.name='Notice']",
|
||||
message: 'Use showNotice(...) instead of new Notice(...).'
|
||||
}
|
||||
],
|
||||
// Upgrade the Obsidian trash rule from warn to error.
|
||||
'obsidianmd/prefer-file-manager-trash-file': 'error',
|
||||
|
||||
// Type assertions
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||
'@typescript-eslint/prefer-as-const': 'warn',
|
||||
|
||||
// Code style
|
||||
eqeqeq: ['error', 'always'], // Require === and !== instead of == and !=
|
||||
'prefer-template': 'warn', // Use template literals instead of string concatenation
|
||||
'@typescript-eslint/array-type': ['warn', { default: 'array' }], // Prefer T[] over Array<T>
|
||||
'prefer-object-spread': 'warn', // Use {...obj} instead of Object.assign()
|
||||
curly: ['warn', 'multi-line'], // Require curly braces for multi-line blocks
|
||||
'no-else-return': 'warn', // Remove unnecessary else after return
|
||||
'obsidianmd/ui/sentence-case': [
|
||||
'warn',
|
||||
{
|
||||
acronyms: ['RRGGBB', 'RRGGBBAA']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/utils/eventActions.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
/* global module */
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
transform: {
|
||||
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.eslint.json" }]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"\\.css$": "<rootDir>/__mocks__/styleMock.js",
|
||||
"\\.md$": "<rootDir>/__mocks__/mdMock.js",
|
||||
},
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.eslint.json' }]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
|
||||
'\\.md$': '<rootDir>/__mocks__/mdMock.js'
|
||||
},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'test_helpers/**/*.ts',
|
||||
'src/utils/format.ts',
|
||||
'src/utils/parse.ts',
|
||||
'src/utils/general.ts',
|
||||
'!test_helpers/**/*.test.ts'
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "obsitexcore",
|
||||
"name": "ObsiTeXcore",
|
||||
"version": "0.0.1",
|
||||
"minAppVersion": "1.3.5",
|
||||
"description": "A powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
|
||||
"author": "Jovi Koikkara",
|
||||
"authorUrl": "https://github.com/YouFoundJK",
|
||||
"fundingUrl": "https://github.com/YouFoundJK",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
"id": "obsitexcore",
|
||||
"name": "ObsiTeXcore",
|
||||
"version": "0.0.1",
|
||||
"minAppVersion": "1.3.5",
|
||||
"description": "A powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
|
||||
"author": "Jovi Koikkara",
|
||||
"authorUrl": "https://github.com/YouFoundJK",
|
||||
"fundingUrl": "https://github.com/YouFoundJK",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
206
mkdocs.yml
206
mkdocs.yml
|
|
@ -5,118 +5,118 @@ repo_name: YouFoundJK/ObsiTeXcore
|
|||
repo_url: https://github.com/YouFoundJK/ObsiTeXcore
|
||||
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: docs/overrides
|
||||
font:
|
||||
text: Work Sans
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.indexes
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- content.code.copy
|
||||
- content.code.select
|
||||
- content.code.annotate
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: white
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to light mode
|
||||
icon:
|
||||
repo: fontawesome/brands/square-github
|
||||
name: material
|
||||
custom_dir: docs/overrides
|
||||
font:
|
||||
text: Work Sans
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.indexes
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- content.code.copy
|
||||
- content.code.select
|
||||
- content.code.annotate
|
||||
palette:
|
||||
- media: '(prefers-color-scheme: light)'
|
||||
scheme: default
|
||||
primary: white
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to dark mode
|
||||
- media: '(prefers-color-scheme: dark)'
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to light mode
|
||||
icon:
|
||||
repo: fontawesome/brands/square-github
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: true
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.keys
|
||||
- pymdownx.caret
|
||||
- attr_list
|
||||
- tables
|
||||
- md_in_html
|
||||
- pymdownx.arithmatex:
|
||||
generic: true
|
||||
- toc:
|
||||
permalink: true
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.keys
|
||||
- pymdownx.caret
|
||||
- attr_list
|
||||
- tables
|
||||
- md_in_html
|
||||
- pymdownx.arithmatex:
|
||||
generic: true
|
||||
|
||||
extra_javascript:
|
||||
- javascripts/mathjax.js
|
||||
- https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js
|
||||
- assets/javascripts/nav-section-links.js
|
||||
- assets/javascripts/theme-logo.js
|
||||
- assets/javascripts/analytics-events.js
|
||||
- javascripts/mathjax.js
|
||||
- https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js
|
||||
- assets/javascripts/nav-section-links.js
|
||||
- assets/javascripts/theme-logo.js
|
||||
- assets/javascripts/analytics-events.js
|
||||
|
||||
extra_css:
|
||||
- assets/stylesheets/site-polish.css
|
||||
- assets/stylesheets/nav-section-links.css
|
||||
- assets/stylesheets/custom-footer.css
|
||||
- assets/stylesheets/site-polish.css
|
||||
- assets/stylesheets/nav-section-links.css
|
||||
- assets/stylesheets/custom-footer.css
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- minify:
|
||||
minify_html: true
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
type: datetime
|
||||
- git-committers:
|
||||
repository: YouFoundJK/ObsiTeXcore
|
||||
cache_dir: .cache/plugin/git-committers
|
||||
branch: main
|
||||
- search
|
||||
- minify:
|
||||
minify_html: true
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
type: datetime
|
||||
- git-committers:
|
||||
repository: YouFoundJK/ObsiTeXcore
|
||||
cache_dir: .cache/plugin/git-committers
|
||||
branch: main
|
||||
|
||||
extra:
|
||||
analytics:
|
||||
provider: custom
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
name: This page was helpful
|
||||
data: 1
|
||||
note: Thanks for your feedback!
|
||||
- icon: material/emoticon-sad-outline
|
||||
name: This page was not helpful
|
||||
data: 0
|
||||
note: Thanks for your feedback!
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/YouFoundJK/ObsiTeXcore
|
||||
analytics:
|
||||
provider: custom
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
name: This page was helpful
|
||||
data: 1
|
||||
note: Thanks for your feedback!
|
||||
- icon: material/emoticon-sad-outline
|
||||
name: This page was not helpful
|
||||
data: 0
|
||||
note: Thanks for your feedback!
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/YouFoundJK/ObsiTeXcore
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Features:
|
||||
- Equation Numbering: features/equations.md
|
||||
- PDF Export: features/pdf-export.md
|
||||
- LaTeX Snippets: features/snippets.md
|
||||
- Equation Search: features/search.md
|
||||
- Quick Preview: features/quick-preview.md
|
||||
- Callout Support: features/callout-support.md
|
||||
- Configuration:
|
||||
- Settings Reference: configuration/settings.md
|
||||
- Home: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Features:
|
||||
- Equation Numbering: features/equations.md
|
||||
- PDF Export: features/pdf-export.md
|
||||
- LaTeX Snippets: features/snippets.md
|
||||
- Equation Search: features/search.md
|
||||
- Quick Preview: features/quick-preview.md
|
||||
- Callout Support: features/callout-support.md
|
||||
- Configuration:
|
||||
- Settings Reference: configuration/settings.md
|
||||
|
|
|
|||
6336
package-lock.json
generated
6336
package-lock.json
generated
File diff suppressed because it is too large
Load diff
154
package.json
154
package.json
|
|
@ -1,78 +1,78 @@
|
|||
{
|
||||
"name": "obsitexcore",
|
||||
"version": "0.0.1",
|
||||
"description": "An Obsidian.md plugin that provides a powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
|
||||
"main": "main.js",
|
||||
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"dev:vault": "node esbuild.config.mjs",
|
||||
"build": "sass src/styles/main.css styles.css && node esbuild.config.mjs production",
|
||||
"prod": "pnpm run compile && pnpm run build",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" && eslint -c eslint.css.config.mjs \"src/**/*.css\"",
|
||||
"lint:eslint": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" -o lint_report.txt --no-color && eslint -c eslint.css.config.mjs \"src/**/*.css\" --no-color",
|
||||
"lint:fix": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" --fix && eslint -c eslint.css.config.mjs \"src/**/*.css\" --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"compile": "tsc -noEmit --skipLibCheck",
|
||||
"test": "jest --ci --silent=true --passWithNoTests",
|
||||
"test-update": "jest --silent=false --updateSnapshot --passWithNoTests",
|
||||
"test-dev": "jest --watch --passWithNoTests",
|
||||
"coverage": "jest --coverage --passWithNoTests",
|
||||
"fl": "prettier --config .prettierrc.json --write \"src/**/*.{ts,js}\"",
|
||||
"ra": "prettier --config .prettierrc.json --write \"src/**/*.{ts,js}\" && pnpm run compile && eslint src -o lint_report.txt --no-color && eslint -c eslint.css.config.mjs \"src/**/*.css\" -o css_report.txt --no-color && jest --ci --silent=true --passWithNoTests",
|
||||
"prod:release": "pnpm run prod && node -e \"const fs=require('fs');const path=require('path');const root=process.cwd();const outputDir=path.join(root,'release-artifacts');fs.rmSync(outputDir,{recursive:true,force:true});fs.mkdirSync(outputDir,{recursive:true});for(const fileName of ['main.js','styles.css']){if(fs.existsSync(path.join(root,fileName))){fs.copyFileSync(path.join(root,fileName),path.join(outputDir,fileName));}}if(fs.existsSync(path.join(root,'manifest.json'))){fs.copyFileSync(path.join(root,'manifest.json'),path.join(outputDir,'manifest.json'));}\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "Jovi Koikkara",
|
||||
"url": "https://github.com/YouFoundJK"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/css": "^1.3.0",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@eslint/json": "^2.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"builtin-modules": "5.2.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"electron": "^42.3.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-svelte": "^0.9.5",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.6.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
"jest-mock-extended": "^4.0.1",
|
||||
"obsidian": "latest",
|
||||
"p-limit": "^7.3.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.100.0",
|
||||
"svelte": "^5.56.1",
|
||||
"ts-jest": "^29.4.11",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.60.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.12.3",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.43.0",
|
||||
"@lezer/common": "^1.5.2",
|
||||
"@lucide/svelte": "^1.17.0",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"flatqueue": "^3.0.0",
|
||||
"monkey-around": "^3.0.0",
|
||||
"sorted-btree": "^2.1.0"
|
||||
}
|
||||
}
|
||||
"name": "obsitexcore",
|
||||
"version": "0.0.1",
|
||||
"description": "An Obsidian.md plugin that provides a powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
|
||||
"main": "main.js",
|
||||
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"dev:vault": "node esbuild.config.mjs",
|
||||
"build": "sass src/styles/main.css styles.css && node esbuild.config.mjs production",
|
||||
"prod": "pnpm run compile && pnpm run build",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" && eslint -c eslint.css.config.mjs \"src/**/*.css\"",
|
||||
"lint:eslint": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" -o lint_report.txt --no-color && eslint -c eslint.css.config.mjs \"src/**/*.css\" --no-color",
|
||||
"lint:fix": "eslint \"src/**/*.{ts,js}\" \"test_helpers/**/*.ts\" \"__mocks__/**/*.ts\" --fix && eslint -c eslint.css.config.mjs \"src/**/*.css\" --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"compile": "tsc -noEmit --skipLibCheck",
|
||||
"test": "jest --ci --silent=true --passWithNoTests",
|
||||
"test-update": "jest --silent=false --updateSnapshot --passWithNoTests",
|
||||
"test-dev": "jest --watch --passWithNoTests",
|
||||
"coverage": "jest --coverage --passWithNoTests",
|
||||
"fl": "prettier --config .prettierrc.json --write \"src/**/*.{ts,js}\"",
|
||||
"ra": "prettier --config .prettierrc.json --write \"src/**/*.{ts,js}\" && pnpm run compile && eslint src -o lint_report.txt --no-color && eslint -c eslint.css.config.mjs \"src/**/*.css\" -o css_report.txt --no-color && jest --ci --silent=true --passWithNoTests",
|
||||
"prod:release": "pnpm run prod && node -e \"const fs=require('fs');const path=require('path');const root=process.cwd();const outputDir=path.join(root,'release-artifacts');fs.rmSync(outputDir,{recursive:true,force:true});fs.mkdirSync(outputDir,{recursive:true});for(const fileName of ['main.js','styles.css']){if(fs.existsSync(path.join(root,fileName))){fs.copyFileSync(path.join(root,fileName),path.join(outputDir,fileName));}}if(fs.existsSync(path.join(root,'manifest.json'))){fs.copyFileSync(path.join(root,'manifest.json'),path.join(outputDir,'manifest.json'));}\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "Jovi Koikkara",
|
||||
"url": "https://github.com/YouFoundJK"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/css": "^1.3.0",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@eslint/json": "^2.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"builtin-modules": "5.2.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"electron": "^42.3.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-svelte": "^0.9.5",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.6.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
"jest-mock-extended": "^4.0.1",
|
||||
"obsidian": "latest",
|
||||
"p-limit": "^7.3.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.100.0",
|
||||
"svelte": "^5.56.1",
|
||||
"ts-jest": "^29.4.11",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.60.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.12.3",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.43.0",
|
||||
"@lezer/common": "^1.5.2",
|
||||
"@lucide/svelte": "^1.17.0",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"flatqueue": "^3.0.0",
|
||||
"monkey-around": "^3.0.0",
|
||||
"sorted-btree": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4607
pnpm-lock.yaml
4607
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
|||
import { editorInfoField } from 'obsidian';
|
||||
import LatexReferencer from 'main';
|
||||
import { CONVERTER, getEqNumberPrefix } from 'utils/format';
|
||||
import type { PluginSettings } from 'features/settings/settings';
|
||||
import type { PluginSettings } from 'settings/settings';
|
||||
import { CALLOUT_PREFIX_REGEX, getCalloutPrefix, isStructuralCalloutLine } from 'utils/parse';
|
||||
|
||||
/**
|
||||
|
|
@ -2,8 +2,8 @@ import { TFile } from 'obsidian';
|
|||
import { CONVERTER, getEqNumberPrefix } from 'utils/format';
|
||||
import { EquationBlock } from 'types';
|
||||
import LatexReferencer from 'main';
|
||||
import { ActiveNoteEquationProvider } from 'features/equations/provider-equation';
|
||||
import type { PluginSettings } from 'features/settings/settings';
|
||||
import { ActiveNoteEquationProvider } from 'core/equations/provider-equation';
|
||||
import type { PluginSettings } from 'settings/settings';
|
||||
|
||||
interface ReferenceInfo {
|
||||
totalCount: number;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { App, MarkdownView, TFile, HeadingSubpathResult, BlockSubpathResult } from 'obsidian';
|
||||
import LatexReferencer from 'main';
|
||||
import { processActiveNoteEquations } from './features/equations/numbering';
|
||||
import { Provider } from './features/linker/provider-link-render';
|
||||
import { processActiveNoteEquations } from '../equations/numbering';
|
||||
import { Provider } from './provider-link-render';
|
||||
|
||||
export class LatexLinkProvider extends Provider {
|
||||
app: App;
|
||||
|
|
@ -11,9 +11,9 @@ import {
|
|||
StandardFonts
|
||||
} from 'pdf-lib';
|
||||
|
||||
import type { PluginSettings } from '../settings/settings';
|
||||
import type { PluginSettings } from '../../settings/settings';
|
||||
|
||||
import type { DocType, PageSizeType, TConfig } from './modal';
|
||||
import type { DocType, PageSizeType, TConfig } from '../../ui/export-pdf/modal';
|
||||
import { TreeNode, getHeadingTree, safeParseFloat, safeParseInt } from './utils';
|
||||
|
||||
interface TPosition {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
Notice,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import type { TConfig } from './modal';
|
||||
import type { TConfig } from '../../ui/export-pdf/modal';
|
||||
import { copyAttributes, fixAnchors, modifyDest } from './utils';
|
||||
import { checkAndFixCalloutMath } from '../../utils/fixer';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import type LatexReferencer from '../../main';
|
||||
import { TextTransformSuggestModal } from './modal';
|
||||
import { TextTransformSuggestModal } from '../../ui/snippets/modal';
|
||||
|
||||
export class SnippetManager {
|
||||
constructor(private plugin: LatexReferencer) {}
|
||||
|
|
|
|||
49
src/main.ts
49
src/main.ts
|
|
@ -11,31 +11,29 @@ import {
|
|||
} from 'obsidian';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { around } from 'monkey-around';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { PluginSettings, DEFAULT_SETTINGS } from './features/settings/settings';
|
||||
import { MathSettingTab } from './features/settings/tab';
|
||||
import { PluginSettings, DEFAULT_SETTINGS } from './settings/settings';
|
||||
import { MathSettingTab } from './settings/tab';
|
||||
|
||||
import { Provider } from './features/linker/provider-link-render';
|
||||
import { LatexLinkProvider } from 'latex-provider';
|
||||
import { createEquationNumberProcessor } from 'features/equations/reading-view-equations';
|
||||
import { CustomMathLinksProcessor } from './features/linker/reading-view-linker';
|
||||
import { setupDOMObserver } from './features/linker/dom-observer';
|
||||
import { createEquationNumberPlugin } from 'features/equations/live-preview-equations';
|
||||
import { createLivePreviewLinkRendererPlugin } from './features/linker/live-preview-link-renderer';
|
||||
import { Provider } from './core/linker/provider-link-render';
|
||||
import { LatexLinkProvider } from './core/linker/latex-provider';
|
||||
import { createEquationNumberProcessor } from 'core/equations/reading-view-equations';
|
||||
import { CustomMathLinksProcessor } from './core/linker/reading-view-linker';
|
||||
import { setupDOMObserver } from './core/linker/dom-observer';
|
||||
import { createEquationNumberPlugin } from 'core/equations/live-preview-equations';
|
||||
import { createLivePreviewLinkRendererPlugin } from './core/linker/live-preview-link-renderer';
|
||||
|
||||
import { insertDisplayMath } from 'utils/plugin';
|
||||
import { LinkAutocomplete } from 'features/search/editor-suggest';
|
||||
import { MathSearchModal } from 'features/search/modal';
|
||||
import { LinkAutocomplete } from 'ui/search/editor-suggest';
|
||||
import { MathSearchModal } from 'ui/search/modal';
|
||||
import { EquationBlock } from 'types';
|
||||
|
||||
// ADDED: Import our new internal patcher function
|
||||
import { patchSuggesterWithQuickPreview } from 'features/quick-preview/patcher';
|
||||
import { processActiveNoteEquations } from './features/equations/numbering';
|
||||
import { ExportConfigModal } from './features/export-pdf/modal';
|
||||
import { patchSuggesterWithQuickPreview } from 'ui/quick-preview/patcher';
|
||||
import { processActiveNoteEquations } from './core/equations/numbering';
|
||||
import { ExportConfigModal } from './ui/export-pdf/modal';
|
||||
import { checkAndFixCalloutMath } from 'utils/fixer';
|
||||
import { traverseFolder } from './features/export-pdf/utils';
|
||||
import { traverseFolder } from 'features/export-pdf/utils';
|
||||
import { SnippetManager } from 'features/snippets/manager';
|
||||
import { processZoteroCleanup } from 'features/zotero-cleanup';
|
||||
import { CustomNoteManager } from 'features/custom-notes/manager';
|
||||
|
|
@ -311,19 +309,22 @@ export default class LatexReferencer extends Plugin {
|
|||
}
|
||||
|
||||
async generateToc(root: TFolder | TFile) {
|
||||
// @ts-ignore
|
||||
const basePath = this.app.vault.adapter.basePath;
|
||||
const toc = path.join(basePath, root.path, '_TOC_.md');
|
||||
const content = `---\ntoc: true\ntitle: ${root.name}\n---\n`;
|
||||
await fs.writeFile(toc, content);
|
||||
const tocPath = root.path === '/' || root.path === '.' ? '_TOC_.md' : `${root.path}/_TOC_.md`;
|
||||
let content = `---\ntoc: true\ntitle: ${root.name || 'Root'}\n---\n`;
|
||||
if (root instanceof TFolder) {
|
||||
const files = traverseFolder(root);
|
||||
for (const file of files) {
|
||||
if (file.name == '_TOC_.md') {
|
||||
if (file.name === '_TOC_.md') {
|
||||
continue;
|
||||
}
|
||||
await fs.appendFile(toc, `[[${file.path}]]\n`);
|
||||
content += `[[${file.path}]]\n`;
|
||||
}
|
||||
}
|
||||
const abstractFile = this.app.vault.getAbstractFileByPath(tocPath);
|
||||
if (abstractFile instanceof TFile) {
|
||||
await this.app.vault.modify(abstractFile, content);
|
||||
} else {
|
||||
await this.app.vault.create(tocPath, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Modifier } from 'obsidian';
|
||||
import type { TConfig } from '../export-pdf/modal';
|
||||
import { LeafArgs } from '../../declarations';
|
||||
import type { TConfig } from '../ui/export-pdf/modal';
|
||||
import { LeafArgs } from '../declarations';
|
||||
|
||||
// Types
|
||||
export const NUMBER_STYLES = ['arabic', 'alph', 'Alph', 'roman', 'Roman'] as const;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { App, PluginSettingTab, Setting, TextAreaComponent } from 'obsidian';
|
||||
import LatexReferencer from '../../main';
|
||||
import LatexReferencer from 'main';
|
||||
import { NUMBER_STYLES } from './settings';
|
||||
import { NoteSuggestModal } from '../custom-notes/modal';
|
||||
import { NoteSuggestModal } from '../ui/custom-notes/modal';
|
||||
|
||||
export class MathSettingTab extends PluginSettingTab {
|
||||
constructor(
|
||||
|
|
@ -9,9 +9,9 @@ import {
|
|||
debounce
|
||||
} from 'obsidian';
|
||||
import * as path from 'path';
|
||||
import { PageSize } from './constant';
|
||||
import LatexReferencer from '../../main';
|
||||
import { exportToPDF, getOutputFile, getOutputPath } from './pdf';
|
||||
import { PageSize } from 'features/export-pdf/constant';
|
||||
import LatexReferencer from 'main';
|
||||
import { exportToPDF, getOutputFile, getOutputPath } from 'features/export-pdf/pdf';
|
||||
import {
|
||||
createWebview,
|
||||
fixDoc,
|
||||
|
|
@ -19,9 +19,16 @@ import {
|
|||
getPatchStyle,
|
||||
renderMarkdown,
|
||||
type ParamType
|
||||
} from './render';
|
||||
import { isNumber, mm2px, px2mm, safeParseFloat, safeParseInt, traverseFolder } from './utils';
|
||||
import Progress from './Progress.svelte';
|
||||
} from 'features/export-pdf/render';
|
||||
import {
|
||||
isNumber,
|
||||
mm2px,
|
||||
px2mm,
|
||||
safeParseFloat,
|
||||
safeParseInt,
|
||||
traverseFolder
|
||||
} from 'features/export-pdf/utils';
|
||||
import Progress from 'features/export-pdf/Progress.svelte';
|
||||
import { mount, unmount } from 'svelte';
|
||||
import pLimit from 'p-limit';
|
||||
export type PageSizeType = Electron.PrintToPDFOptions['pageSize'];
|
||||
|
|
@ -15,11 +15,11 @@ import {
|
|||
} from 'obsidian';
|
||||
import LatexReferencer from 'main';
|
||||
import { EquationBlock } from 'types';
|
||||
import { LEAF_OPTION_TO_ARGS } from '../settings/settings';
|
||||
import { LEAF_OPTION_TO_ARGS } from '../../settings/settings';
|
||||
import { getModifierNameInPlatform, openFileAndSelectPosition } from 'utils/obsidian';
|
||||
import { insertBlockIdIfNotExist } from 'utils/plugin';
|
||||
import { MathSearchModal } from './modal';
|
||||
import { ActiveNoteEquationProvider } from 'features/equations/provider-equation';
|
||||
import { ActiveNoteEquationProvider } from 'core/equations/provider-equation';
|
||||
|
||||
export type ScoredEquationBlock = { match: SearchResult; block: EquationBlock };
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import {
|
|||
BUILTIN_TEXT_TRANSFORM_SNIPPETS,
|
||||
runTextTransformSnippet,
|
||||
TextTransformSnippet
|
||||
} from './transforms';
|
||||
} from '../../features/snippets/transforms';
|
||||
|
||||
export class TextTransformSuggestModal extends FuzzySuggestModal<TextTransformSnippet> {
|
||||
constructor(
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
type DebugCapablePlugin = {
|
||||
settings?: { debug?: boolean };
|
||||
app?: { vault?: { adapter?: { basePath?: string } } };
|
||||
app?: {
|
||||
vault?: {
|
||||
adapter?: {
|
||||
basePath?: string;
|
||||
append(path: string, data: string): Promise<void>;
|
||||
write(path: string, data: string): Promise<void>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
let sequence = 0;
|
||||
|
|
@ -11,9 +16,9 @@ let sequence = 0;
|
|||
function getDefaultLogPath(plugin?: DebugCapablePlugin): string {
|
||||
const basePath = plugin?.app?.vault?.adapter?.basePath;
|
||||
if (typeof basePath === 'string' && basePath.length > 0) {
|
||||
return path.join(basePath, 'latex-referencer-debug.log');
|
||||
return `${basePath}/latex-referencer-debug.log`;
|
||||
}
|
||||
return path.join(process.cwd(), 'latex-referencer-debug.log');
|
||||
return 'latex-referencer-debug.log';
|
||||
}
|
||||
|
||||
function canDebug(plugin?: DebugCapablePlugin): boolean {
|
||||
|
|
@ -22,7 +27,10 @@ function canDebug(plugin?: DebugCapablePlugin): boolean {
|
|||
|
||||
function writeDebugLine(line: string, plugin?: DebugCapablePlugin) {
|
||||
try {
|
||||
fs.appendFileSync(getDefaultLogPath(plugin), `${line}\n`, { encoding: 'utf-8' });
|
||||
const adapter = plugin?.app?.vault?.adapter;
|
||||
if (adapter && typeof adapter.append === 'function') {
|
||||
adapter.append('latex-referencer-debug.log', `${line}\n`);
|
||||
}
|
||||
} catch (_) {
|
||||
// Best-effort only. Console logging should still work.
|
||||
}
|
||||
|
|
@ -53,9 +61,11 @@ export function logDebugEvent(
|
|||
}
|
||||
|
||||
export function clearDebugLog(plugin?: DebugCapablePlugin) {
|
||||
const logPath = getDefaultLogPath(plugin);
|
||||
try {
|
||||
fs.writeFileSync(logPath, '', { encoding: 'utf-8' });
|
||||
const adapter = plugin?.app?.vault?.adapter;
|
||||
if (adapter && typeof adapter.write === 'function') {
|
||||
adapter.write('latex-referencer-debug.log', '');
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore write failures.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, TFile } from 'obsidian';
|
||||
import { PluginSettings } from '../features/settings/settings';
|
||||
import { PluginSettings } from '../settings/settings';
|
||||
|
||||
const ROMAN = [
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -1,203 +1,193 @@
|
|||
import {
|
||||
App,
|
||||
CachedMetadata,
|
||||
EventRef,
|
||||
FileManager,
|
||||
Keymap,
|
||||
MetadataCache,
|
||||
RenderContext,
|
||||
Scope,
|
||||
SecretStorage,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
UserEvent,
|
||||
Workspace,
|
||||
} from "obsidian";
|
||||
import { FileBuilder } from "./FileBuilder";
|
||||
import { MockVault } from "./MockVault";
|
||||
App,
|
||||
CachedMetadata,
|
||||
EventRef,
|
||||
FileManager,
|
||||
Keymap,
|
||||
MetadataCache,
|
||||
RenderContext,
|
||||
Scope,
|
||||
SecretStorage,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
UserEvent,
|
||||
Workspace
|
||||
} from 'obsidian';
|
||||
import { FileBuilder } from './FileBuilder';
|
||||
import { MockVault } from './MockVault';
|
||||
|
||||
const normalizeSlashes = (value: string): string => value.replace(/\\/g, "/");
|
||||
const normalizeSlashes = (value: string): string => value.replace(/\\/g, '/');
|
||||
const joinPath = (...parts: string[]): string => {
|
||||
const joined = parts
|
||||
.map(normalizeSlashes)
|
||||
.filter((part, index) => part !== "" || index === 0)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/");
|
||||
return joined === "" ? "." : joined;
|
||||
const joined = parts
|
||||
.map(normalizeSlashes)
|
||||
.filter((part, index) => part !== '' || index === 0)
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/');
|
||||
return joined === '' ? '.' : joined;
|
||||
};
|
||||
const baseName = (path: string): string => {
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
||||
};
|
||||
|
||||
export class MockCache implements MetadataCache {
|
||||
private cache: Map<string, CachedMetadata>;
|
||||
private cache: Map<string, CachedMetadata>;
|
||||
|
||||
constructor(cache: Map<string, CachedMetadata>) {
|
||||
this.cache = cache;
|
||||
}
|
||||
constructor(cache: Map<string, CachedMetadata>) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
getCache(path: string): CachedMetadata | null {
|
||||
return this.cache.get(joinPath("/", path)) || null;
|
||||
}
|
||||
getCache(path: string): CachedMetadata | null {
|
||||
return this.cache.get(joinPath('/', path)) || null;
|
||||
}
|
||||
|
||||
getFileCache(file: TFile): CachedMetadata | null {
|
||||
return this.getCache(file.path);
|
||||
}
|
||||
getFileCache(file: TFile): CachedMetadata | null {
|
||||
return this.getCache(file.path);
|
||||
}
|
||||
|
||||
// Below here is not implemented.
|
||||
// Below here is not implemented.
|
||||
|
||||
getFirstLinkpathDest(linkpath: string, sourcePath: string): TFile | null {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
fileToLinktext(
|
||||
file: TFile,
|
||||
sourcePath: string,
|
||||
omitMdExtension?: boolean
|
||||
): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
resolvedLinks: Record<string, Record<string, number>> = {};
|
||||
unresolvedLinks: Record<string, Record<string, number>> = {};
|
||||
on(
|
||||
name: "changed",
|
||||
callback: (file: TFile, data: string, cache: CachedMetadata) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(
|
||||
name: "deleted",
|
||||
callback: (file: TFile, prevCache: CachedMetadata | null) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(name: "resolve", callback: (file: TFile) => unknown, ctx?: unknown): EventRef;
|
||||
on(name: "resolved", callback: () => unknown, ctx?: unknown): EventRef;
|
||||
on(
|
||||
name: unknown,
|
||||
callback: unknown,
|
||||
ctx?: unknown
|
||||
): import("obsidian").EventRef {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
offref(ref: EventRef): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getFirstLinkpathDest(linkpath: string, sourcePath: string): TFile | null {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
fileToLinktext(file: TFile, sourcePath: string, omitMdExtension?: boolean): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
resolvedLinks: Record<string, Record<string, number>> = {};
|
||||
unresolvedLinks: Record<string, Record<string, number>> = {};
|
||||
on(
|
||||
name: 'changed',
|
||||
callback: (file: TFile, data: string, cache: CachedMetadata) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(
|
||||
name: 'deleted',
|
||||
callback: (file: TFile, prevCache: CachedMetadata | null) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(name: 'resolve', callback: (file: TFile) => unknown, ctx?: unknown): EventRef;
|
||||
on(name: 'resolved', callback: () => unknown, ctx?: unknown): EventRef;
|
||||
on(name: unknown, callback: unknown, ctx?: unknown): import('obsidian').EventRef {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
offref(ref: EventRef): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockApp implements App {
|
||||
keymap: Keymap = {} as Keymap;
|
||||
scope: Scope = {} as Scope;
|
||||
workspace: Workspace = {} as Workspace;
|
||||
lastEvent: UserEvent | null = null;
|
||||
renderContext: RenderContext = {} as RenderContext;
|
||||
secretStorage: SecretStorage = {} as SecretStorage;
|
||||
keymap: Keymap = {} as Keymap;
|
||||
scope: Scope = {} as Scope;
|
||||
workspace: Workspace = {} as Workspace;
|
||||
lastEvent: UserEvent | null = null;
|
||||
renderContext: RenderContext = {} as RenderContext;
|
||||
secretStorage: SecretStorage = {} as SecretStorage;
|
||||
|
||||
fileManager: FileManager = {} as FileManager;
|
||||
metadataCache: MetadataCache;
|
||||
vault: MockVault;
|
||||
plugins: unknown = {};
|
||||
internalPlugins: unknown = {};
|
||||
isDarkMode(): boolean {
|
||||
return false;
|
||||
}
|
||||
fileManager: FileManager = {} as FileManager;
|
||||
metadataCache: MetadataCache;
|
||||
vault: MockVault;
|
||||
plugins: unknown = {};
|
||||
internalPlugins: unknown = {};
|
||||
isDarkMode(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
loadLocalStorage(): void {
|
||||
// mock implementation
|
||||
}
|
||||
loadLocalStorage(): void {
|
||||
// mock implementation
|
||||
}
|
||||
|
||||
saveLocalStorage(): void {
|
||||
// mock implementation
|
||||
}
|
||||
constructor(vault: MockVault, cache: MockCache) {
|
||||
this.vault = vault;
|
||||
this.metadataCache = cache;
|
||||
}
|
||||
saveLocalStorage(): void {
|
||||
// mock implementation
|
||||
}
|
||||
constructor(vault: MockVault, cache: MockCache) {
|
||||
this.vault = vault;
|
||||
this.metadataCache = cache;
|
||||
}
|
||||
}
|
||||
|
||||
interface FileTree<T> {
|
||||
[key: string]: { t: "file"; v: T } | { t: "folder"; v: FileTree<T> };
|
||||
[key: string]: { t: 'file'; v: T } | { t: 'folder'; v: FileTree<T> };
|
||||
}
|
||||
|
||||
function toPathMap<T>(tree: FileTree<T>): Map<string, T> {
|
||||
const recurse = (t: FileTree<T>, path: string): [string, T][] =>
|
||||
Object.entries(t).flatMap(([name, v]) =>
|
||||
v.t === "file"
|
||||
? [[joinPath(path, name), v.v]]
|
||||
: recurse(v.v, joinPath(path, name))
|
||||
);
|
||||
return new Map(recurse(tree, "/"));
|
||||
const recurse = (t: FileTree<T>, path: string): [string, T][] =>
|
||||
Object.entries(t).flatMap(([name, v]) =>
|
||||
v.t === 'file' ? [[joinPath(path, name), v.v]] : recurse(v.v, joinPath(path, name))
|
||||
);
|
||||
return new Map(recurse(tree, '/'));
|
||||
}
|
||||
|
||||
export class MockAppBuilder {
|
||||
children: TAbstractFile[];
|
||||
metadata: FileTree<CachedMetadata>;
|
||||
contents: FileTree<string>;
|
||||
path: string;
|
||||
children: TAbstractFile[];
|
||||
metadata: FileTree<CachedMetadata>;
|
||||
contents: FileTree<string>;
|
||||
path: string;
|
||||
|
||||
static make() {
|
||||
return new MockAppBuilder("/", [], {}, {});
|
||||
static make() {
|
||||
return new MockAppBuilder('/', [], {}, {});
|
||||
}
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
children: TAbstractFile[] = [],
|
||||
contents: FileTree<string> = {},
|
||||
metadata: FileTree<CachedMetadata> = {}
|
||||
) {
|
||||
this.path = joinPath('/', path);
|
||||
this.children = children;
|
||||
this.metadata = metadata;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
file(filename: string, builder: FileBuilder): MockAppBuilder {
|
||||
const file = new TFile();
|
||||
file.name = filename;
|
||||
|
||||
const [contents, metadata] = builder.done();
|
||||
|
||||
return new MockAppBuilder(
|
||||
this.path,
|
||||
[...this.children, file],
|
||||
{ ...this.contents, [filename]: { t: 'file', v: contents } },
|
||||
{ ...this.metadata, [filename]: { t: 'file', v: metadata } }
|
||||
);
|
||||
}
|
||||
|
||||
folder(f: MockAppBuilder) {
|
||||
return new MockAppBuilder(
|
||||
this.path,
|
||||
[...this.children, f.makeFolder()],
|
||||
{ ...this.contents, [f.path]: { t: 'folder', v: f.contents } },
|
||||
{ ...this.metadata, [f.path]: { t: 'folder', v: f.metadata } }
|
||||
);
|
||||
}
|
||||
|
||||
private makeFolder(): TFolder {
|
||||
const folder = new TFolder();
|
||||
folder.name = this.path === '/' ? '' : baseName(this.path);
|
||||
if (folder.name === '') {
|
||||
(folder as unknown as { isRootVal: boolean }).isRootVal = true;
|
||||
}
|
||||
this.children.forEach(f => (f.parent = folder));
|
||||
folder.children = [...this.children];
|
||||
return folder;
|
||||
}
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
children: TAbstractFile[] = [],
|
||||
contents: FileTree<string> = {},
|
||||
metadata: FileTree<CachedMetadata> = {}
|
||||
) {
|
||||
this.path = joinPath("/", path);
|
||||
this.children = children;
|
||||
this.metadata = metadata;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
file(filename: string, builder: FileBuilder): MockAppBuilder {
|
||||
const file = new TFile();
|
||||
file.name = filename;
|
||||
|
||||
const [contents, metadata] = builder.done();
|
||||
|
||||
return new MockAppBuilder(
|
||||
this.path,
|
||||
[...this.children, file],
|
||||
{ ...this.contents, [filename]: { t: "file", v: contents } },
|
||||
{ ...this.metadata, [filename]: { t: "file", v: metadata } }
|
||||
);
|
||||
}
|
||||
|
||||
folder(f: MockAppBuilder) {
|
||||
return new MockAppBuilder(
|
||||
this.path,
|
||||
[...this.children, f.makeFolder()],
|
||||
{ ...this.contents, [f.path]: { t: "folder", v: f.contents } },
|
||||
{ ...this.metadata, [f.path]: { t: "folder", v: f.metadata } }
|
||||
);
|
||||
}
|
||||
|
||||
private makeFolder(): TFolder {
|
||||
const folder = new TFolder();
|
||||
folder.name = this.path === "/" ? "" : baseName(this.path);
|
||||
if (folder.name === "") {
|
||||
(folder as unknown as { isRootVal: boolean }).isRootVal = true;
|
||||
}
|
||||
this.children.forEach((f) => (f.parent = folder));
|
||||
folder.children = [...this.children];
|
||||
return folder;
|
||||
}
|
||||
|
||||
done(): MockApp {
|
||||
return new MockApp(
|
||||
new MockVault(this.makeFolder(), toPathMap(this.contents)),
|
||||
new MockCache(toPathMap(this.metadata))
|
||||
);
|
||||
}
|
||||
done(): MockApp {
|
||||
return new MockApp(
|
||||
new MockVault(this.makeFolder(), toPathMap(this.contents)),
|
||||
new MockCache(toPathMap(this.metadata))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FileBuilder, ListBuilder } from "./FileBuilder";
|
||||
import { FileBuilder, ListBuilder } from './FileBuilder';
|
||||
|
||||
/**
|
||||
* FileBuilder Tests
|
||||
|
|
@ -16,37 +16,35 @@ import { FileBuilder, ListBuilder } from "./FileBuilder";
|
|||
* the FileBuilder.
|
||||
*/
|
||||
|
||||
describe("frontmatter tests", () => {
|
||||
it("single k/v", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ key: "value" })
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
describe('frontmatter tests', () => {
|
||||
it('single k/v', () => {
|
||||
const [contents, metadata] = new FileBuilder().frontmatter({ key: 'value' }).done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"---
|
||||
key: value
|
||||
---
|
||||
"
|
||||
`);
|
||||
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
key: "value",
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 18 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
key: 'value',
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 18 }
|
||||
}
|
||||
}
|
||||
});
|
||||
it("multiple k/v", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({
|
||||
title: "event",
|
||||
startTime: "13:00",
|
||||
endTime: "15:00",
|
||||
})
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('multiple k/v', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({
|
||||
title: 'event',
|
||||
startTime: '13:00',
|
||||
endTime: '15:00'
|
||||
})
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"---
|
||||
title: event
|
||||
startTime: 13:00
|
||||
|
|
@ -54,45 +52,43 @@ describe("frontmatter tests", () => {
|
|||
---
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
title: "event",
|
||||
startTime: "13:00",
|
||||
endTime: "15:00",
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 4, col: 3, offset: 52 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
title: 'event',
|
||||
startTime: '13:00',
|
||||
endTime: '15:00',
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 4, col: 3, offset: 52 }
|
||||
}
|
||||
}
|
||||
});
|
||||
it("arrays", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ values: ["A", "B", "C"] })
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('arrays', () => {
|
||||
const [contents, metadata] = new FileBuilder().frontmatter({ values: ['A', 'B', 'C'] }).done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"---
|
||||
values: [A,B,C]
|
||||
---
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
values: ["A", "B", "C"],
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 23 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
values: ['A', 'B', 'C'],
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 23 }
|
||||
}
|
||||
}
|
||||
});
|
||||
it("frontmatter should be hoisted", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ hello: 1 })
|
||||
.text("F1ST!!!!!")
|
||||
.frontmatter({ world: 2 })
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('frontmatter should be hoisted', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ hello: 1 })
|
||||
.text('F1ST!!!!!')
|
||||
.frontmatter({ world: 2 })
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"---
|
||||
hello: 1
|
||||
world: 2
|
||||
|
|
@ -100,93 +96,91 @@ describe("frontmatter tests", () => {
|
|||
F1ST!!!!!
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
hello: 1,
|
||||
world: 2,
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 3, col: 3, offset: 25 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
frontmatter: {
|
||||
hello: 1,
|
||||
world: 2,
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 3, col: 3, offset: 25 }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("headings", () => {
|
||||
it("single h1", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.heading(1, "Hello world!")
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
describe('headings', () => {
|
||||
it('single h1', () => {
|
||||
const [contents, metadata] = new FileBuilder().heading(1, 'Hello world!').done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"# Hello world!
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 14, offset: 14 },
|
||||
},
|
||||
heading: "Hello world!",
|
||||
level: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 14, offset: 14 }
|
||||
},
|
||||
heading: 'Hello world!',
|
||||
level: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
it("multiple h2s", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.heading(2, "First heading")
|
||||
.heading(2, "Second heading")
|
||||
.heading(2, "Third heading")
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('multiple h2s', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.heading(2, 'First heading')
|
||||
.heading(2, 'Second heading')
|
||||
.heading(2, 'Third heading')
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"## First heading
|
||||
## Second heading
|
||||
## Third heading
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 16, offset: 16 },
|
||||
},
|
||||
heading: "First heading",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 17 },
|
||||
end: { line: 1, col: 17, offset: 34 },
|
||||
},
|
||||
heading: "Second heading",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 35 },
|
||||
end: { line: 2, col: 16, offset: 51 },
|
||||
},
|
||||
heading: "Third heading",
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 16, offset: 16 }
|
||||
},
|
||||
heading: 'First heading',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 17 },
|
||||
end: { line: 1, col: 17, offset: 34 }
|
||||
},
|
||||
heading: 'Second heading',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 35 },
|
||||
end: { line: 2, col: 16, offset: 51 }
|
||||
},
|
||||
heading: 'Third heading',
|
||||
level: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
it("nested headers", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.heading(2, "First heading")
|
||||
.heading(2, "Second heading")
|
||||
.heading(3, "Nested heading")
|
||||
.heading(3, "Nested 2")
|
||||
.heading(4, "double nest")
|
||||
.heading(4, "double nest 2")
|
||||
.heading(3, "Nested 3")
|
||||
.heading(2, "Another thing")
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('nested headers', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.heading(2, 'First heading')
|
||||
.heading(2, 'Second heading')
|
||||
.heading(3, 'Nested heading')
|
||||
.heading(3, 'Nested 2')
|
||||
.heading(4, 'double nest')
|
||||
.heading(4, 'double nest 2')
|
||||
.heading(3, 'Nested 3')
|
||||
.heading(2, 'Another thing')
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"## First heading
|
||||
## Second heading
|
||||
### Nested heading
|
||||
|
|
@ -197,290 +191,282 @@ describe("headings", () => {
|
|||
## Another thing
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 16, offset: 16 },
|
||||
},
|
||||
heading: "First heading",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 17 },
|
||||
end: { line: 1, col: 17, offset: 34 },
|
||||
},
|
||||
heading: "Second heading",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 35 },
|
||||
end: { line: 2, col: 18, offset: 53 },
|
||||
},
|
||||
heading: "Nested heading",
|
||||
level: 3,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 54 },
|
||||
end: { line: 3, col: 12, offset: 66 },
|
||||
},
|
||||
heading: "Nested 2",
|
||||
level: 3,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 4, col: 0, offset: 67 },
|
||||
end: { line: 4, col: 16, offset: 83 },
|
||||
},
|
||||
heading: "double nest",
|
||||
level: 4,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 5, col: 0, offset: 84 },
|
||||
end: { line: 5, col: 18, offset: 102 },
|
||||
},
|
||||
heading: "double nest 2",
|
||||
level: 4,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 6, col: 0, offset: 103 },
|
||||
end: { line: 6, col: 12, offset: 115 },
|
||||
},
|
||||
heading: "Nested 3",
|
||||
level: 3,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 7, col: 0, offset: 116 },
|
||||
end: { line: 7, col: 16, offset: 132 },
|
||||
},
|
||||
heading: "Another thing",
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 16, offset: 16 }
|
||||
},
|
||||
heading: 'First heading',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 17 },
|
||||
end: { line: 1, col: 17, offset: 34 }
|
||||
},
|
||||
heading: 'Second heading',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 35 },
|
||||
end: { line: 2, col: 18, offset: 53 }
|
||||
},
|
||||
heading: 'Nested heading',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 54 },
|
||||
end: { line: 3, col: 12, offset: 66 }
|
||||
},
|
||||
heading: 'Nested 2',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 4, col: 0, offset: 67 },
|
||||
end: { line: 4, col: 16, offset: 83 }
|
||||
},
|
||||
heading: 'double nest',
|
||||
level: 4
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 5, col: 0, offset: 84 },
|
||||
end: { line: 5, col: 18, offset: 102 }
|
||||
},
|
||||
heading: 'double nest 2',
|
||||
level: 4
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 6, col: 0, offset: 103 },
|
||||
end: { line: 6, col: 12, offset: 115 }
|
||||
},
|
||||
heading: 'Nested 3',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 7, col: 0, offset: 116 },
|
||||
end: { line: 7, col: 16, offset: 132 }
|
||||
},
|
||||
heading: 'Another thing',
|
||||
level: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("text", () => {
|
||||
it("one line", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text("hello, world!")
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
describe('text', () => {
|
||||
it('one line', () => {
|
||||
const [contents, metadata] = new FileBuilder().text('hello, world!').done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"hello, world!
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({});
|
||||
});
|
||||
it("a few lines", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text("hello, world!")
|
||||
.text("goodbye, galaxy!")
|
||||
.text("p.s. fake text is hard to be creative with")
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
expect(metadata).toEqual({});
|
||||
});
|
||||
it('a few lines', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text('hello, world!')
|
||||
.text('goodbye, galaxy!')
|
||||
.text('p.s. fake text is hard to be creative with')
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"hello, world!
|
||||
goodbye, galaxy!
|
||||
p.s. fake text is hard to be creative with
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({});
|
||||
});
|
||||
expect(metadata).toEqual({});
|
||||
});
|
||||
});
|
||||
describe("lists", () => {
|
||||
it("single item", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.list(new ListBuilder().item("one item"))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
describe('lists', () => {
|
||||
it('single item', () => {
|
||||
const [contents, metadata] = new FileBuilder().list(new ListBuilder().item('one item')).done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"- one item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 10, offset: 10 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 10, offset: 10 }
|
||||
},
|
||||
parent: -1
|
||||
}
|
||||
]
|
||||
});
|
||||
it("two items", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.list(new ListBuilder().item("one item"))
|
||||
.list(new ListBuilder().item("two item"))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('two items', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.list(new ListBuilder().item('one item'))
|
||||
.list(new ListBuilder().item('two item'))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"- one item
|
||||
- two item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 10, offset: 10 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 11 },
|
||||
end: { line: 1, col: 10, offset: 21 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 10, offset: 10 }
|
||||
},
|
||||
parent: -1
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 11 },
|
||||
end: { line: 1, col: 10, offset: 21 }
|
||||
},
|
||||
parent: -1
|
||||
}
|
||||
]
|
||||
});
|
||||
it("empty first line", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text("")
|
||||
.list(new ListBuilder().item("one item").item("two item"))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('empty first line', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text('')
|
||||
.list(new ListBuilder().item('one item').item('two item'))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"
|
||||
- one item
|
||||
- two item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 1 },
|
||||
end: { line: 1, col: 10, offset: 11 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 12 },
|
||||
end: { line: 2, col: 10, offset: 22 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 1 },
|
||||
end: { line: 1, col: 10, offset: 11 }
|
||||
},
|
||||
parent: -1
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 12 },
|
||||
end: { line: 2, col: 10, offset: 22 }
|
||||
},
|
||||
parent: -1
|
||||
}
|
||||
]
|
||||
});
|
||||
it("empty two lines", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text("")
|
||||
.text("")
|
||||
.list(new ListBuilder().item("one item").item("two item"))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('empty two lines', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.text('')
|
||||
.text('')
|
||||
.list(new ListBuilder().item('one item').item('two item'))
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
||||
- one item
|
||||
- two item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 2 },
|
||||
end: { line: 2, col: 10, offset: 12 },
|
||||
},
|
||||
parent: -2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 13 },
|
||||
end: { line: 3, col: 10, offset: 23 },
|
||||
},
|
||||
parent: -2,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 2 },
|
||||
end: { line: 2, col: 10, offset: 12 }
|
||||
},
|
||||
parent: -2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 13 },
|
||||
end: { line: 3, col: 10, offset: 23 }
|
||||
},
|
||||
parent: -2
|
||||
}
|
||||
]
|
||||
});
|
||||
it("tasks", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.list(
|
||||
new ListBuilder()
|
||||
.item("task 1", false)
|
||||
.item("task 2", true)
|
||||
.item("task 3", false)
|
||||
.item("regular item")
|
||||
)
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
});
|
||||
it('tasks', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.list(
|
||||
new ListBuilder()
|
||||
.item('task 1', false)
|
||||
.item('task 2', true)
|
||||
.item('task 3', false)
|
||||
.item('regular item')
|
||||
)
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"- [ ] task 1
|
||||
- [x] task 2
|
||||
- [ ] task 3
|
||||
- regular item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 12, offset: 12 },
|
||||
},
|
||||
parent: -1,
|
||||
task: " ",
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 13 },
|
||||
end: { line: 1, col: 12, offset: 25 },
|
||||
},
|
||||
parent: -1,
|
||||
task: "x",
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 26 },
|
||||
end: { line: 2, col: 12, offset: 38 },
|
||||
},
|
||||
parent: -1,
|
||||
task: " ",
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 39 },
|
||||
end: { line: 3, col: 14, offset: 53 },
|
||||
},
|
||||
parent: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(metadata).toEqual({
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 0, col: 12, offset: 12 }
|
||||
},
|
||||
parent: -1,
|
||||
task: ' '
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 1, col: 0, offset: 13 },
|
||||
end: { line: 1, col: 12, offset: 25 }
|
||||
},
|
||||
parent: -1,
|
||||
task: 'x'
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 2, col: 0, offset: 26 },
|
||||
end: { line: 2, col: 12, offset: 38 }
|
||||
},
|
||||
parent: -1,
|
||||
task: ' '
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 39 },
|
||||
end: { line: 3, col: 14, offset: 53 }
|
||||
},
|
||||
parent: -1
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it("big example", () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ hello: "world" })
|
||||
.heading(2, "Journal")
|
||||
.text("this is a journal entry!")
|
||||
.text("and a second line!")
|
||||
.heading(2, "My list")
|
||||
.list(
|
||||
new ListBuilder()
|
||||
.item("first list item")
|
||||
.item("second list item")
|
||||
.item("to-do", false)
|
||||
.item("done", true)
|
||||
.item("nested list")
|
||||
.list(
|
||||
new ListBuilder()
|
||||
.item("nested list item")
|
||||
.item("another nested item")
|
||||
)
|
||||
)
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
it('big example', () => {
|
||||
const [contents, metadata] = new FileBuilder()
|
||||
.frontmatter({ hello: 'world' })
|
||||
.heading(2, 'Journal')
|
||||
.text('this is a journal entry!')
|
||||
.text('and a second line!')
|
||||
.heading(2, 'My list')
|
||||
.list(
|
||||
new ListBuilder()
|
||||
.item('first list item')
|
||||
.item('second list item')
|
||||
.item('to-do', false)
|
||||
.item('done', true)
|
||||
.item('nested list')
|
||||
.list(new ListBuilder().item('nested list item').item('another nested item'))
|
||||
)
|
||||
.done();
|
||||
expect(contents).toMatchInlineSnapshot(`
|
||||
"---
|
||||
hello: world
|
||||
---
|
||||
|
|
@ -497,84 +483,84 @@ it("big example", () => {
|
|||
- another nested item
|
||||
"
|
||||
`);
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 21 },
|
||||
end: { line: 3, col: 10, offset: 31 },
|
||||
},
|
||||
heading: "Journal",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 6, col: 0, offset: 76 },
|
||||
end: { line: 6, col: 10, offset: 86 },
|
||||
},
|
||||
heading: "My list",
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 7, col: 0, offset: 87 },
|
||||
end: { line: 7, col: 17, offset: 104 },
|
||||
},
|
||||
parent: -7,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 8, col: 0, offset: 105 },
|
||||
end: { line: 8, col: 18, offset: 123 },
|
||||
},
|
||||
parent: -7,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 9, col: 0, offset: 124 },
|
||||
end: { line: 9, col: 11, offset: 135 },
|
||||
},
|
||||
parent: -7,
|
||||
task: " ",
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 10, col: 0, offset: 136 },
|
||||
end: { line: 10, col: 10, offset: 146 },
|
||||
},
|
||||
parent: -7,
|
||||
task: "x",
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 11, col: 0, offset: 147 },
|
||||
end: { line: 11, col: 13, offset: 160 },
|
||||
},
|
||||
parent: -7,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 12, col: 2, offset: 163 },
|
||||
end: { line: 12, col: 22, offset: 183 },
|
||||
},
|
||||
parent: 11,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 13, col: 2, offset: 186 },
|
||||
end: { line: 13, col: 25, offset: 209 },
|
||||
},
|
||||
parent: 11,
|
||||
},
|
||||
],
|
||||
frontmatter: {
|
||||
hello: "world",
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 20 },
|
||||
},
|
||||
expect(metadata).toEqual({
|
||||
headings: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 3, col: 0, offset: 21 },
|
||||
end: { line: 3, col: 10, offset: 31 }
|
||||
},
|
||||
});
|
||||
heading: 'Journal',
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 6, col: 0, offset: 76 },
|
||||
end: { line: 6, col: 10, offset: 86 }
|
||||
},
|
||||
heading: 'My list',
|
||||
level: 2
|
||||
}
|
||||
],
|
||||
listItems: [
|
||||
{
|
||||
position: {
|
||||
start: { line: 7, col: 0, offset: 87 },
|
||||
end: { line: 7, col: 17, offset: 104 }
|
||||
},
|
||||
parent: -7
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 8, col: 0, offset: 105 },
|
||||
end: { line: 8, col: 18, offset: 123 }
|
||||
},
|
||||
parent: -7
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 9, col: 0, offset: 124 },
|
||||
end: { line: 9, col: 11, offset: 135 }
|
||||
},
|
||||
parent: -7,
|
||||
task: ' '
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 10, col: 0, offset: 136 },
|
||||
end: { line: 10, col: 10, offset: 146 }
|
||||
},
|
||||
parent: -7,
|
||||
task: 'x'
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 11, col: 0, offset: 147 },
|
||||
end: { line: 11, col: 13, offset: 160 }
|
||||
},
|
||||
parent: -7
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 12, col: 2, offset: 163 },
|
||||
end: { line: 12, col: 22, offset: 183 }
|
||||
},
|
||||
parent: 11
|
||||
},
|
||||
{
|
||||
position: {
|
||||
start: { line: 13, col: 2, offset: 186 },
|
||||
end: { line: 13, col: 25, offset: 209 }
|
||||
},
|
||||
parent: 11
|
||||
}
|
||||
],
|
||||
frontmatter: {
|
||||
hello: 'world',
|
||||
position: {
|
||||
start: { line: 0, col: 0, offset: 0 },
|
||||
end: { line: 2, col: 3, offset: 20 }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,216 +1,200 @@
|
|||
import { CachedMetadata, ListItemCache, Loc, Pos } from "obsidian";
|
||||
import { CachedMetadata, ListItemCache, Loc, Pos } from 'obsidian';
|
||||
|
||||
type ListItem = {
|
||||
type: "item";
|
||||
text: string;
|
||||
checkbox: "x" | " " | undefined;
|
||||
type: 'item';
|
||||
text: string;
|
||||
checkbox: 'x' | ' ' | undefined;
|
||||
};
|
||||
|
||||
type ListBlock = {
|
||||
type: "list";
|
||||
items: (ListItem | ListBlock)[];
|
||||
type: 'list';
|
||||
items: (ListItem | ListBlock)[];
|
||||
};
|
||||
|
||||
type ListBlockItem = {
|
||||
type: "listitem";
|
||||
text: string;
|
||||
checkbox: " " | "x" | undefined;
|
||||
depth: number;
|
||||
parent: number;
|
||||
type: 'listitem';
|
||||
text: string;
|
||||
checkbox: ' ' | 'x' | undefined;
|
||||
depth: number;
|
||||
parent: number;
|
||||
};
|
||||
|
||||
const makeListItems = (
|
||||
list: ListBlock,
|
||||
startingLine: number,
|
||||
depth: number = 0
|
||||
list: ListBlock,
|
||||
startingLine: number,
|
||||
depth: number = 0
|
||||
): ListBlockItem[] => {
|
||||
if (startingLine === 0) {
|
||||
startingLine = 1;
|
||||
if (startingLine === 0) {
|
||||
startingLine = 1;
|
||||
}
|
||||
const lines: ListBlockItem[] = [];
|
||||
for (const i of list.items) {
|
||||
if (i.type === 'item') {
|
||||
lines.push({
|
||||
type: 'listitem',
|
||||
text: i.text,
|
||||
checkbox: i.checkbox,
|
||||
depth,
|
||||
parent: depth === 0 ? -startingLine : startingLine
|
||||
});
|
||||
} else {
|
||||
lines.push(...makeListItems(i, startingLine + lines.length - 1, depth + 1));
|
||||
}
|
||||
const lines: ListBlockItem[] = [];
|
||||
for (const i of list.items) {
|
||||
if (i.type === "item") {
|
||||
lines.push({
|
||||
type: "listitem",
|
||||
text: i.text,
|
||||
checkbox: i.checkbox,
|
||||
depth,
|
||||
parent: depth === 0 ? -startingLine : startingLine,
|
||||
});
|
||||
} else {
|
||||
lines.push(
|
||||
...makeListItems(i, startingLine + lines.length - 1, depth + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
type FileBlock =
|
||||
| {
|
||||
type: "heading";
|
||||
text: string;
|
||||
level: number;
|
||||
| {
|
||||
type: 'heading';
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
| ListBlock
|
||||
| { type: "frontmatter"; key: string; text: unknown }
|
||||
| { type: "text"; text: string };
|
||||
| ListBlock
|
||||
| { type: 'frontmatter'; key: string; text: unknown }
|
||||
| { type: 'text'; text: string };
|
||||
|
||||
const makeFile = (
|
||||
lines: FileBlock[],
|
||||
tabChars = " ".repeat(4)
|
||||
): [string, CachedMetadata] => {
|
||||
let lineNum = 0;
|
||||
let content = "";
|
||||
const makeFile = (lines: FileBlock[], tabChars = ' '.repeat(4)): [string, CachedMetadata] => {
|
||||
let lineNum = 0;
|
||||
let content = '';
|
||||
|
||||
const appendLine = (line: string): Pos => {
|
||||
const start: Loc = { line: lineNum, col: 0, offset: content.length };
|
||||
content += `${line}\n`;
|
||||
const end: Loc = {
|
||||
line: lineNum++,
|
||||
col: line.length,
|
||||
offset: content.length - 1,
|
||||
};
|
||||
return { start, end };
|
||||
const appendLine = (line: string): Pos => {
|
||||
const start: Loc = { line: lineNum, col: 0, offset: content.length };
|
||||
content += `${line}\n`;
|
||||
const end: Loc = {
|
||||
line: lineNum++,
|
||||
col: line.length,
|
||||
offset: content.length - 1
|
||||
};
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
const meta: CachedMetadata = {};
|
||||
const meta: CachedMetadata = {};
|
||||
|
||||
const frontmatter = lines.flatMap((l) =>
|
||||
l.type === "frontmatter" ? l : []
|
||||
);
|
||||
const frontmatter = lines.flatMap(l => (l.type === 'frontmatter' ? l : []));
|
||||
|
||||
if (frontmatter.length > 0) {
|
||||
const data: Record<string, unknown> = {};
|
||||
const { start } = appendLine("---");
|
||||
for (const elt of frontmatter) {
|
||||
if (Array.isArray(elt.text)) {
|
||||
appendLine(`${elt.key}: [${(elt.text as unknown[]).join(',')}]`);
|
||||
} else {
|
||||
appendLine(`${elt.key}: ${String(elt.text)}`);
|
||||
}
|
||||
data[elt.key] = elt.text;
|
||||
}
|
||||
const { end } = appendLine("---");
|
||||
meta.frontmatter = {
|
||||
position: { start, end },
|
||||
...data,
|
||||
};
|
||||
if (frontmatter.length > 0) {
|
||||
const data: Record<string, unknown> = {};
|
||||
const { start } = appendLine('---');
|
||||
for (const elt of frontmatter) {
|
||||
if (Array.isArray(elt.text)) {
|
||||
appendLine(`${elt.key}: [${(elt.text as unknown[]).join(',')}]`);
|
||||
} else {
|
||||
appendLine(`${elt.key}: ${String(elt.text)}`);
|
||||
}
|
||||
data[elt.key] = elt.text;
|
||||
}
|
||||
const { end } = appendLine('---');
|
||||
meta.frontmatter = {
|
||||
position: { start, end },
|
||||
...data
|
||||
};
|
||||
}
|
||||
|
||||
const blocks = lines.flatMap((l) => (l.type !== "frontmatter" ? l : []));
|
||||
for (const block of blocks) {
|
||||
switch (block.type) {
|
||||
case "heading": {
|
||||
if (!meta.headings) {
|
||||
meta.headings = [];
|
||||
}
|
||||
const position = appendLine(
|
||||
"#".repeat(block.level) + " " + block.text
|
||||
);
|
||||
meta.headings.push({
|
||||
position,
|
||||
heading: block.text,
|
||||
level: block.level,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
case "list": {
|
||||
if (!meta.listItems) {
|
||||
meta.listItems = [];
|
||||
}
|
||||
for (const item of makeListItems(block, lineNum)) {
|
||||
const indent = tabChars.repeat(item.depth);
|
||||
const position = appendLine(
|
||||
indent +
|
||||
"- " +
|
||||
(item.checkbox ? `[${item.checkbox}] ` : "") +
|
||||
item.text
|
||||
);
|
||||
if (indent.length > 0) {
|
||||
position.start.col += indent.length - 2;
|
||||
position.start.offset += indent.length - 2;
|
||||
}
|
||||
|
||||
const listItem: ListItemCache = {
|
||||
position,
|
||||
parent: item.parent,
|
||||
};
|
||||
if (item.checkbox) {
|
||||
listItem["task"] = item.checkbox;
|
||||
}
|
||||
|
||||
meta.listItems.push(listItem);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
case "text":
|
||||
appendLine(block.text);
|
||||
const blocks = lines.flatMap(l => (l.type !== 'frontmatter' ? l : []));
|
||||
for (const block of blocks) {
|
||||
switch (block.type) {
|
||||
case 'heading': {
|
||||
if (!meta.headings) {
|
||||
meta.headings = [];
|
||||
}
|
||||
}
|
||||
const position = appendLine('#'.repeat(block.level) + ' ' + block.text);
|
||||
meta.headings.push({
|
||||
position,
|
||||
heading: block.text,
|
||||
level: block.level
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
return [content, meta];
|
||||
case 'list': {
|
||||
if (!meta.listItems) {
|
||||
meta.listItems = [];
|
||||
}
|
||||
for (const item of makeListItems(block, lineNum)) {
|
||||
const indent = tabChars.repeat(item.depth);
|
||||
const position = appendLine(
|
||||
indent + '- ' + (item.checkbox ? `[${item.checkbox}] ` : '') + item.text
|
||||
);
|
||||
if (indent.length > 0) {
|
||||
position.start.col += indent.length - 2;
|
||||
position.start.offset += indent.length - 2;
|
||||
}
|
||||
|
||||
const listItem: ListItemCache = {
|
||||
position,
|
||||
parent: item.parent
|
||||
};
|
||||
if (item.checkbox) {
|
||||
listItem['task'] = item.checkbox;
|
||||
}
|
||||
|
||||
meta.listItems.push(listItem);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
case 'text':
|
||||
appendLine(block.text);
|
||||
}
|
||||
}
|
||||
|
||||
return [content, meta];
|
||||
};
|
||||
|
||||
export class ListBuilder {
|
||||
items: (ListItem | ListBlock)[] = [];
|
||||
items: (ListItem | ListBlock)[] = [];
|
||||
|
||||
constructor(items: (ListItem | ListBlock)[] = []) {
|
||||
this.items = items;
|
||||
}
|
||||
constructor(items: (ListItem | ListBlock)[] = []) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
item(text: string, checkbox?: boolean) {
|
||||
const i: ListItem = {
|
||||
type: "item",
|
||||
text,
|
||||
checkbox:
|
||||
checkbox === true ? "x" : checkbox === false ? " " : undefined,
|
||||
};
|
||||
return new ListBuilder([...this.items, i]);
|
||||
}
|
||||
item(text: string, checkbox?: boolean) {
|
||||
const i: ListItem = {
|
||||
type: 'item',
|
||||
text,
|
||||
checkbox: checkbox === true ? 'x' : checkbox === false ? ' ' : undefined
|
||||
};
|
||||
return new ListBuilder([...this.items, i]);
|
||||
}
|
||||
|
||||
list(lb: ListBuilder) {
|
||||
return new ListBuilder([...this.items, lb.done()]);
|
||||
}
|
||||
list(lb: ListBuilder) {
|
||||
return new ListBuilder([...this.items, lb.done()]);
|
||||
}
|
||||
|
||||
done(): ListBlock {
|
||||
return { type: "list", items: this.items };
|
||||
}
|
||||
done(): ListBlock {
|
||||
return { type: 'list', items: this.items };
|
||||
}
|
||||
}
|
||||
|
||||
export class FileBuilder {
|
||||
private lines: FileBlock[];
|
||||
private lines: FileBlock[];
|
||||
|
||||
constructor(lines: FileBlock[] = []) {
|
||||
this.lines = lines;
|
||||
}
|
||||
constructor(lines: FileBlock[] = []) {
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
frontmatter(frontmatter: Record<string, unknown>): FileBuilder {
|
||||
const frontmatterLines = Object.entries(frontmatter).map(
|
||||
([k, v]): FileBlock => ({ type: "frontmatter", key: k, text: v })
|
||||
);
|
||||
return new FileBuilder([...this.lines, ...frontmatterLines]);
|
||||
}
|
||||
frontmatter(frontmatter: Record<string, unknown>): FileBuilder {
|
||||
const frontmatterLines = Object.entries(frontmatter).map(
|
||||
([k, v]): FileBlock => ({ type: 'frontmatter', key: k, text: v })
|
||||
);
|
||||
return new FileBuilder([...this.lines, ...frontmatterLines]);
|
||||
}
|
||||
|
||||
heading(level: number, text: string): FileBuilder {
|
||||
return new FileBuilder([
|
||||
...this.lines,
|
||||
{ type: "heading", level, text },
|
||||
]);
|
||||
}
|
||||
heading(level: number, text: string): FileBuilder {
|
||||
return new FileBuilder([...this.lines, { type: 'heading', level, text }]);
|
||||
}
|
||||
|
||||
text(text: string): FileBuilder {
|
||||
return new FileBuilder([...this.lines, { type: "text", text }]);
|
||||
}
|
||||
text(text: string): FileBuilder {
|
||||
return new FileBuilder([...this.lines, { type: 'text', text }]);
|
||||
}
|
||||
|
||||
list(list: ListBuilder): FileBuilder {
|
||||
return new FileBuilder([...this.lines, list.done()]);
|
||||
}
|
||||
list(list: ListBuilder): FileBuilder {
|
||||
return new FileBuilder([...this.lines, list.done()]);
|
||||
}
|
||||
|
||||
done() {
|
||||
return makeFile(this.lines);
|
||||
}
|
||||
done() {
|
||||
return makeFile(this.lines);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
209
test_helpers/MockVault.test.ts
Normal file
209
test_helpers/MockVault.test.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { MockAppBuilder } from './AppBuilder';
|
||||
import { FileBuilder } from './FileBuilder';
|
||||
import { TFile, TFolder } from 'obsidian';
|
||||
|
||||
describe('MockVault API tests', () => {
|
||||
it('verify vault basic properties and name', () => {
|
||||
const app = MockAppBuilder.make().file('test.md', new FileBuilder().text('hello')).done();
|
||||
const vault = app.vault;
|
||||
|
||||
expect(vault.getName()).toBe('Mock Vault');
|
||||
expect(vault.getRoot().isRootVal).toBe(true);
|
||||
expect(vault.getRoot().name).toBe('');
|
||||
});
|
||||
|
||||
it('verify getFileByPath, getFolderByPath, getFiles, getMarkdownFiles, getAllFolders', async () => {
|
||||
const app = MockAppBuilder.make()
|
||||
.file('root.md', new FileBuilder().text('root'))
|
||||
.folder(
|
||||
new MockAppBuilder('folder1')
|
||||
.file('sub.md', new FileBuilder().text('sub'))
|
||||
.file('sub.txt', new FileBuilder().text('text file'))
|
||||
)
|
||||
.done();
|
||||
const vault = app.vault;
|
||||
|
||||
// getFileByPath
|
||||
const rootFile = vault.getFileByPath('root.md');
|
||||
expect(rootFile).toBeInstanceOf(TFile);
|
||||
expect(rootFile?.name).toBe('root.md');
|
||||
|
||||
const missingFile = vault.getFileByPath('does-not-exist.md');
|
||||
expect(missingFile).toBeNull();
|
||||
|
||||
const notAFile = vault.getFileByPath('folder1');
|
||||
expect(notAFile).toBeNull();
|
||||
|
||||
// getFolderByPath (returns null in MockVault, but we call it to cover the method)
|
||||
const folder = vault.getFolderByPath('folder1');
|
||||
expect(folder).toBeNull();
|
||||
|
||||
// getFiles & getMarkdownFiles
|
||||
const allFiles = vault.getFiles();
|
||||
expect(allFiles.length).toBe(3); // root.md, sub.md, sub.txt
|
||||
const mdFiles = vault.getMarkdownFiles();
|
||||
expect(mdFiles.length).toBe(2); // root.md, sub.md
|
||||
|
||||
// getAllFolders
|
||||
const allFolders = vault.getAllFolders();
|
||||
expect(allFolders.length).toBe(2); // root, folder1
|
||||
});
|
||||
|
||||
it('verify create, createFolder, delete, trash, modify, copy, read errors', async () => {
|
||||
const app = MockAppBuilder.make().done();
|
||||
const vault = app.vault;
|
||||
|
||||
// createFolder
|
||||
const folder1 = await vault.createFolder('folder1');
|
||||
expect(folder1).toBeInstanceOf(TFolder);
|
||||
expect(folder1.name).toBe('folder1');
|
||||
|
||||
// create file
|
||||
const file1 = await vault.create('folder1/file1.md', 'hello world');
|
||||
expect(file1).toBeInstanceOf(TFile);
|
||||
expect(file1.name).toBe('file1.md');
|
||||
|
||||
// create duplicate error
|
||||
expect(() => vault.create('folder1/file1.md', 'dup')).toThrow('File already exists.');
|
||||
|
||||
// create file inside non-existent parent error
|
||||
expect(() => vault.create('folder2/file2.md', 'no parent')).toThrow(
|
||||
'Parent path is not folder.'
|
||||
);
|
||||
|
||||
// read file
|
||||
const data = await vault.read(file1);
|
||||
expect(data).toBe('hello world');
|
||||
|
||||
// read invalid file path error
|
||||
const fakeFile = new TFile();
|
||||
fakeFile.name = 'fake.md';
|
||||
// path is empty, which joins to "/." or throws
|
||||
expect(() => vault.read(fakeFile)).toThrow('File at path /fake.md does not have contents');
|
||||
|
||||
// modify file
|
||||
await vault.modify(file1, 'modified contents');
|
||||
expect(await vault.read(file1)).toBe('modified contents');
|
||||
|
||||
// copy file
|
||||
const copiedFile = await vault.copy(file1, 'folder1/copied.md');
|
||||
expect(copiedFile).toBeInstanceOf(TFile);
|
||||
expect(copiedFile.name).toBe('copied.md');
|
||||
expect(await vault.read(copiedFile)).toBe('modified contents');
|
||||
|
||||
// copy folder error (not supported)
|
||||
const fakeFolderAsFile = new TFolder();
|
||||
await expect(vault.copy(fakeFolderAsFile as any, 'copied_folder')).rejects.toThrow(
|
||||
'MockVault.copy only supports TFile in this mock.'
|
||||
);
|
||||
|
||||
// delete file
|
||||
await vault.delete(copiedFile);
|
||||
expect(vault.getFileByPath('folder1/copied.md')).toBeNull();
|
||||
|
||||
// trash file (delegates to delete)
|
||||
await vault.trash(file1, false);
|
||||
expect(vault.getFileByPath('folder1/file1.md')).toBeNull();
|
||||
});
|
||||
|
||||
it('verify rename file and folder', async () => {
|
||||
const app = MockAppBuilder.make()
|
||||
.folder(new MockAppBuilder('folder1').file('sub.md', new FileBuilder().text('sub')))
|
||||
.done();
|
||||
const vault = app.vault;
|
||||
|
||||
const subFile = vault.getFileByPath('folder1/sub.md');
|
||||
expect(subFile).not.toBeNull();
|
||||
|
||||
// rename file
|
||||
if (subFile) {
|
||||
await vault.rename(subFile, 'folder1/new_sub.md');
|
||||
expect(vault.getFileByPath('folder1/sub.md')).toBeNull();
|
||||
const renamed = vault.getFileByPath('folder1/new_sub.md');
|
||||
expect(renamed).not.toBeNull();
|
||||
expect(await vault.read(renamed!)).toBe('sub\n');
|
||||
}
|
||||
|
||||
// create another folder
|
||||
const folder2 = await vault.createFolder('folder2');
|
||||
|
||||
// rename folder
|
||||
const folder1 = vault.getAbstractFileByPath('folder1');
|
||||
expect(folder1).toBeInstanceOf(TFolder);
|
||||
if (folder1) {
|
||||
await vault.rename(folder1, 'folder2/folder1_moved');
|
||||
expect(vault.getAbstractFileByPath('folder1')).toBeNull();
|
||||
const movedFolder = vault.getAbstractFileByPath('folder2/folder1_moved');
|
||||
expect(movedFolder).toBeInstanceOf(TFolder);
|
||||
const movedFile = vault.getFileByPath('folder2/folder1_moved/new_sub.md');
|
||||
expect(movedFile).not.toBeNull();
|
||||
expect(await vault.read(movedFile!)).toBe('sub\n');
|
||||
}
|
||||
});
|
||||
|
||||
it('verify rename errors', async () => {
|
||||
const app = MockAppBuilder.make().file('file1.md', new FileBuilder().text('hello')).done();
|
||||
const vault = app.vault;
|
||||
|
||||
const file1 = vault.getFileByPath('file1.md');
|
||||
expect(file1).not.toBeNull();
|
||||
|
||||
if (file1) {
|
||||
// rename to non-existent folder
|
||||
expect(() => vault.rename(file1, 'nonexistent/file1.md')).toThrow(
|
||||
'No such folder: nonexistent'
|
||||
);
|
||||
|
||||
// rename file without contents
|
||||
const fakeFile = new TFile();
|
||||
fakeFile.name = 'fake.md';
|
||||
fakeFile.parent = vault.getRoot();
|
||||
expect(() => vault.rename(fakeFile, 'file_renamed.md')).toThrow(
|
||||
'File did not have contents: fake.md'
|
||||
);
|
||||
}
|
||||
|
||||
// rename invalid type error
|
||||
const fakeObject = { path: 'fake' } as any;
|
||||
expect(() => vault.rename(fakeObject, 'renamed')).toThrow('File is not a file or folder');
|
||||
});
|
||||
|
||||
it('verify unimplemented / stub methods on vault, app, cache', async () => {
|
||||
const app = MockAppBuilder.make().file('test.md', new FileBuilder().text('hello')).done();
|
||||
const vault = app.vault;
|
||||
const cache = app.metadataCache;
|
||||
|
||||
// MockApp stubs
|
||||
expect(app.isDarkMode()).toBe(false);
|
||||
app.loadLocalStorage();
|
||||
app.saveLocalStorage();
|
||||
|
||||
// MockCache stubs
|
||||
const file = vault.getFileByPath('test.md')!;
|
||||
expect(() => cache.getFirstLinkpathDest('link', 'source')).toThrow('Method not implemented.');
|
||||
expect(() => cache.fileToLinktext(file, 'source')).toThrow('Method not implemented.');
|
||||
expect(() => cache.on('changed', () => {})).toThrow('Method not implemented.');
|
||||
expect(() => cache.off('changed', () => {})).toThrow('Method not implemented.');
|
||||
expect(() => cache.offref({} as any)).toThrow('Method not implemented.');
|
||||
expect(() => cache.trigger('changed')).toThrow('Method not implemented.');
|
||||
expect(() => cache.tryTrigger({} as any, [])).toThrow('Method not implemented.');
|
||||
|
||||
// MockVault unimplemented methods
|
||||
expect(() => vault.append(file, 'data')).toThrow('Method not implemented.');
|
||||
expect(() => vault.appendBinary(file, new ArrayBuffer(0))).toThrow('Method not implemented.');
|
||||
expect(() => vault.createBinary('path.png', new ArrayBuffer(0))).toThrow(
|
||||
'Method not implemented.'
|
||||
);
|
||||
expect(() => vault.readBinary(file)).toThrow('Method not implemented.');
|
||||
expect(() => vault.modifyBinary(file, new ArrayBuffer(0))).toThrow('Method not implemented.');
|
||||
expect(() => vault.getResourcePath(file)).toThrow('Method not implemented.');
|
||||
expect(() => vault.on('create', () => {})).toThrow('Method not implemented.');
|
||||
expect(() => vault.off('create', () => {})).toThrow('Method not implemented.');
|
||||
expect(() => vault.offref({} as any)).toThrow('Method not implemented.');
|
||||
expect(() => vault.trigger('create')).toThrow('Method not implemented.');
|
||||
expect(() => vault.tryTrigger({} as any, [])).toThrow('Method not implemented.');
|
||||
expect(() => vault.process(file, d => d)).toThrow('Method not implemented.');
|
||||
vault.loadLocalStorage();
|
||||
vault.saveLocalStorage();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,305 +1,281 @@
|
|||
import {
|
||||
DataAdapter,
|
||||
DataWriteOptions,
|
||||
EventRef,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
Vault,
|
||||
} from "obsidian";
|
||||
DataAdapter,
|
||||
DataWriteOptions,
|
||||
EventRef,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
Vault
|
||||
} from 'obsidian';
|
||||
|
||||
const normalizeSlashes = (value: string): string => value.replace(/\\/g, "/");
|
||||
const normalizeSlashes = (value: string): string => value.replace(/\\/g, '/');
|
||||
const joinPath = (...parts: string[]): string => {
|
||||
const joined = parts
|
||||
.map(normalizeSlashes)
|
||||
.filter((part, index) => (part !== "" && part !== ".") || index === 0)
|
||||
.join("/")
|
||||
.replace(/\/+/, "/");
|
||||
return joined === "" ? "." : joined;
|
||||
const joined = parts
|
||||
.map(normalizeSlashes)
|
||||
.filter((part, index) => (part !== '' && part !== '.') || index === 0)
|
||||
.join('/')
|
||||
.replace(/\/+/, '/');
|
||||
return joined === '' ? '.' : joined;
|
||||
};
|
||||
const dirName = (path: string): string => {
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
return lastSlash > 0 ? normalized.slice(0, lastSlash) : "";
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
return lastSlash > 0 ? normalized.slice(0, lastSlash) : '';
|
||||
};
|
||||
const baseName = (path: string): string => {
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
||||
const normalized = normalizeSlashes(path);
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
||||
};
|
||||
|
||||
const collectChildren = (folder: TFolder): TAbstractFile[] => {
|
||||
return folder.children.flatMap((f) => {
|
||||
if (f instanceof TFolder) {
|
||||
return [f, ...collectChildren(f)];
|
||||
} else {
|
||||
return f;
|
||||
}
|
||||
});
|
||||
return folder.children.flatMap(f => {
|
||||
if (f instanceof TFolder) {
|
||||
return [f, ...collectChildren(f)];
|
||||
} else {
|
||||
return f;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export class MockVault implements Vault {
|
||||
root: TFolder;
|
||||
contents: Map<string, string>;
|
||||
root: TFolder;
|
||||
contents: Map<string, string>;
|
||||
|
||||
constructor(root: TFolder, contents: Map<string, string>) {
|
||||
this.root = root;
|
||||
this.contents = contents;
|
||||
constructor(root: TFolder, contents: Map<string, string>) {
|
||||
this.root = root;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
adapter: DataAdapter = {} as DataAdapter;
|
||||
configDir: string = '';
|
||||
|
||||
getName(): string {
|
||||
return 'Mock Vault';
|
||||
}
|
||||
|
||||
getAllLoadedFiles(): TAbstractFile[] {
|
||||
return [this.root, ...collectChildren(this.root)];
|
||||
}
|
||||
|
||||
getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^\//, '');
|
||||
const normalizedPath = normalize(path);
|
||||
|
||||
if (normalizedPath === '') {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
adapter: DataAdapter = {} as DataAdapter;
|
||||
configDir: string = "";
|
||||
const parts = normalizedPath.split('/');
|
||||
let current: TAbstractFile | undefined = this.root;
|
||||
|
||||
getName(): string {
|
||||
return "Mock Vault";
|
||||
}
|
||||
|
||||
getAllLoadedFiles(): TAbstractFile[] {
|
||||
return [this.root, ...collectChildren(this.root)];
|
||||
}
|
||||
|
||||
getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^\//, '');
|
||||
const normalizedPath = normalize(path);
|
||||
|
||||
if (normalizedPath === '') {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
const parts = normalizedPath.split('/');
|
||||
let current: TAbstractFile | undefined = this.root;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!(current instanceof TFolder)) {
|
||||
return null;
|
||||
}
|
||||
current = current.children.find(child => child.name === part);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return current || null;
|
||||
}
|
||||
|
||||
read(file: TFile): Promise<string> {
|
||||
const p = joinPath("/", file.path);
|
||||
const contents = this.contents.get(p);
|
||||
if (!contents) {
|
||||
throw new Error(`File at path ${p} does not have contents`);
|
||||
}
|
||||
return Promise.resolve(contents);
|
||||
}
|
||||
|
||||
getFileByPath(path: string): TFile | null {
|
||||
const file = this.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
return file;
|
||||
}
|
||||
for (const part of parts) {
|
||||
if (!(current instanceof TFolder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getFolderByPath(path: string): TFolder | null {
|
||||
console.warn(`MockVault.getFolderByPath called with: ${path}`);
|
||||
}
|
||||
current = current.children.find(child => child.name === part);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getRoot(): TFolder {
|
||||
return this.root;
|
||||
return current || null;
|
||||
}
|
||||
|
||||
read(file: TFile): Promise<string> {
|
||||
const p = joinPath('/', file.path);
|
||||
const contents = this.contents.get(p);
|
||||
if (!contents) {
|
||||
throw new Error(`File at path ${p} does not have contents`);
|
||||
}
|
||||
return Promise.resolve(contents);
|
||||
}
|
||||
|
||||
getFileByPath(path: string): TFile | null {
|
||||
const file = this.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getFolderByPath(path: string): TFolder | null {
|
||||
console.warn(`MockVault.getFolderByPath called with: ${path}`);
|
||||
return null;
|
||||
}
|
||||
getRoot(): TFolder {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
cachedRead(file: TFile): Promise<string> {
|
||||
return this.read(file);
|
||||
}
|
||||
|
||||
getFiles(): TFile[] {
|
||||
return this.getAllLoadedFiles().flatMap(f => (f instanceof TFile ? f : []));
|
||||
}
|
||||
|
||||
getMarkdownFiles(): TFile[] {
|
||||
return this.getFiles().filter(f => f.extension.toLowerCase() === 'md');
|
||||
}
|
||||
|
||||
private setParent(path: string, f: TAbstractFile) {
|
||||
const parentPath = dirName(path);
|
||||
const folder = this.getAbstractFileByPath(parentPath);
|
||||
if (folder instanceof TFolder) {
|
||||
f.parent = folder;
|
||||
folder.children.push(f);
|
||||
return;
|
||||
}
|
||||
throw new Error('Parent path is not folder.');
|
||||
}
|
||||
|
||||
create(path: string, data: string, options?: DataWriteOptions): Promise<TFile> {
|
||||
if (this.getAbstractFileByPath(path)) {
|
||||
throw new Error('File already exists.');
|
||||
}
|
||||
const file = new TFile();
|
||||
file.name = baseName(path);
|
||||
this.setParent(path, file);
|
||||
this.contents.set(joinPath('/', path), data);
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
createFolder(path: string): Promise<TFolder> {
|
||||
const folder = new TFolder();
|
||||
folder.name = baseName(path);
|
||||
this.setParent(path, folder);
|
||||
return Promise.resolve(folder);
|
||||
}
|
||||
|
||||
delete(file: TAbstractFile, force?: boolean): Promise<void> {
|
||||
if (file.parent) {
|
||||
const index = file.parent.children.indexOf(file);
|
||||
if (index > -1) {
|
||||
file.parent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this.contents.delete(joinPath('/', file.path));
|
||||
return Promise.resolve();
|
||||
}
|
||||
trash(file: TAbstractFile, system: boolean): Promise<void> {
|
||||
return this.delete(file);
|
||||
}
|
||||
process(file: TFile, fn: (data: string) => string, options?: DataWriteOptions): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getAllFolders(): TFolder[] {
|
||||
return this.getAllLoadedFiles().filter((f): f is TFolder => f instanceof TFolder);
|
||||
}
|
||||
rename(file: TAbstractFile, newPath: string): Promise<void> {
|
||||
const newParentPath = dirName(newPath);
|
||||
const newParent = this.getAbstractFileByPath(newParentPath);
|
||||
if (!(newParent instanceof TFolder)) {
|
||||
throw new Error(`No such folder: ${newParentPath}`);
|
||||
}
|
||||
|
||||
cachedRead(file: TFile): Promise<string> {
|
||||
return this.read(file);
|
||||
if (file.parent) {
|
||||
const index = file.parent.children.indexOf(file);
|
||||
if (index > -1) {
|
||||
file.parent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
newParent.children.push(file);
|
||||
|
||||
getFiles(): TFile[] {
|
||||
return this.getAllLoadedFiles().flatMap((f) =>
|
||||
f instanceof TFile ? f : []
|
||||
);
|
||||
}
|
||||
if (file instanceof TFile) {
|
||||
const contents = this.contents.get(joinPath('/', file.path));
|
||||
if (!contents) {
|
||||
throw new Error(`File did not have contents: ${file.path}`);
|
||||
}
|
||||
this.contents.delete(joinPath('/', file.path));
|
||||
|
||||
getMarkdownFiles(): TFile[] {
|
||||
return this.getFiles().filter(
|
||||
(f) => f.extension.toLowerCase() === "md"
|
||||
);
|
||||
}
|
||||
file.parent = newParent;
|
||||
file.name = baseName(newPath);
|
||||
this.contents.set(joinPath('/', file.path), contents);
|
||||
} else if (file instanceof TFolder) {
|
||||
const filesAndContents = collectChildren(file)
|
||||
.flatMap(f => (f instanceof TFile ? f : []))
|
||||
.map((f): [TFile, string] => {
|
||||
const contents = this.contents.get(joinPath('/', f.path));
|
||||
if (!contents) {
|
||||
throw new Error(`File did not have contents: ${f.path}`);
|
||||
}
|
||||
this.contents.delete(joinPath('/', f.path));
|
||||
return [f, contents];
|
||||
});
|
||||
|
||||
private setParent(path: string, f: TAbstractFile) {
|
||||
const parentPath = dirName(path);
|
||||
const folder = this.getAbstractFileByPath(parentPath);
|
||||
if (folder instanceof TFolder) {
|
||||
f.parent = folder;
|
||||
folder.children.push(f);
|
||||
return;
|
||||
}
|
||||
throw new Error("Parent path is not folder.");
|
||||
}
|
||||
file.parent = newParent;
|
||||
file.name = baseName(newPath);
|
||||
|
||||
create(path: string, data: string, options?: DataWriteOptions): Promise<TFile> {
|
||||
if (this.getAbstractFileByPath(path)) {
|
||||
throw new Error("File already exists.");
|
||||
}
|
||||
const file = new TFile();
|
||||
file.name = baseName(path);
|
||||
this.setParent(path, file);
|
||||
this.contents.set(path, data);
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
createFolder(path: string): Promise<TFolder> {
|
||||
const folder = new TFolder();
|
||||
folder.name = baseName(path);
|
||||
this.setParent(path, folder);
|
||||
return Promise.resolve(folder);
|
||||
for (const [f, contents] of filesAndContents) {
|
||||
this.contents.set(joinPath('/', f.path), contents);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`File is not a file or folder: ${file.path}`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
delete(file: TAbstractFile, force?: boolean): Promise<void> {
|
||||
if (file.parent) {
|
||||
file.parent.children.remove(file);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
trash(file: TAbstractFile, system: boolean): Promise<void> {
|
||||
return this.delete(file);
|
||||
}
|
||||
process(file: TFile, fn: (data: string) => string, options?: DataWriteOptions): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
modify(file: TFile, data: string, options?: DataWriteOptions): Promise<void> {
|
||||
this.contents.set(joinPath('/', file.path), data);
|
||||
return Promise.resolve();
|
||||
}
|
||||
loadLocalStorage(): void {}
|
||||
|
||||
getAllFolders(): TFolder[] {
|
||||
return this.getAllLoadedFiles().filter((f): f is TFolder => f instanceof TFolder);
|
||||
}
|
||||
rename(file: TAbstractFile, newPath: string): Promise<void> {
|
||||
const newParentPath = dirName(newPath);
|
||||
const newParent = this.getAbstractFileByPath(newParentPath);
|
||||
if (!(newParent instanceof TFolder)) {
|
||||
throw new Error(`No such folder: ${newParentPath}`);
|
||||
}
|
||||
saveLocalStorage(): void {}
|
||||
|
||||
if (file instanceof TFile) {
|
||||
const contents = this.contents.get(file.path);
|
||||
if (!contents) {
|
||||
throw new Error(`File did not have contents: ${file.path}`);
|
||||
}
|
||||
this.contents.delete(file.path);
|
||||
async copy<T extends TAbstractFile>(file: T, newPath: string): Promise<T> {
|
||||
if (!(file instanceof TFile)) {
|
||||
throw new Error('MockVault.copy only supports TFile in this mock.');
|
||||
}
|
||||
const data = await this.read(file);
|
||||
const newFile = await this.create(newPath, data);
|
||||
return newFile as unknown as T;
|
||||
}
|
||||
|
||||
file.parent = newParent;
|
||||
file.name = baseName(newPath);
|
||||
this.contents.set(file.path, contents);
|
||||
} else if (file instanceof TFolder) {
|
||||
const filesAndContents = collectChildren(file)
|
||||
.flatMap((f) => (f instanceof TFile ? f : []))
|
||||
.map((f): [TFile, string] => {
|
||||
const contents = this.contents.get(f.path);
|
||||
if (!contents) {
|
||||
throw new Error(
|
||||
`File did not have contents: ${f.path}`
|
||||
);
|
||||
}
|
||||
this.contents.delete(f.path);
|
||||
return [f, contents];
|
||||
});
|
||||
on(name: 'create', callback: (file: TAbstractFile) => unknown, ctx?: unknown): EventRef;
|
||||
on(name: 'modify', callback: (file: TAbstractFile) => unknown, ctx?: unknown): EventRef;
|
||||
on(name: 'delete', callback: (file: TAbstractFile) => unknown, ctx?: unknown): EventRef;
|
||||
on(
|
||||
name: 'rename',
|
||||
callback: (file: TAbstractFile, oldPath: string) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(name: 'closed', callback: () => unknown, ctx?: unknown): EventRef;
|
||||
on(name: unknown, callback: unknown, ctx?: unknown): EventRef {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
offref(ref: EventRef): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
append(file: TFile, data: string, options?: DataWriteOptions): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
file.parent = newParent;
|
||||
file.name = baseName(newPath);
|
||||
appendBinary(file: TFile, data: ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
for (const [f, contents] of filesAndContents) {
|
||||
this.contents.set(f.path, contents);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`File is not a file or folder: ${file.path}`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
createBinary(path: string, data: ArrayBuffer, options?: DataWriteOptions): Promise<TFile> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
readBinary(file: TFile): Promise<ArrayBuffer> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
modify(file: TFile, data: string, options?: DataWriteOptions): Promise<void> {
|
||||
this.contents.set(file.path, data);
|
||||
return Promise.resolve();
|
||||
}
|
||||
loadLocalStorage(): void {
|
||||
}
|
||||
modifyBinary(file: TFile, data: ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
saveLocalStorage(): void {
|
||||
}
|
||||
|
||||
async copy<T extends TAbstractFile>(file: T, newPath: string): Promise<T> {
|
||||
if (!(file instanceof TFile)) {
|
||||
throw new Error("MockVault.copy only supports TFile in this mock.");
|
||||
}
|
||||
const data = await this.read(file);
|
||||
const newFile = await this.create(newPath, data);
|
||||
return newFile as unknown as T;
|
||||
}
|
||||
|
||||
on(
|
||||
name: "create",
|
||||
callback: (file: TAbstractFile) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(
|
||||
name: "modify",
|
||||
callback: (file: TAbstractFile) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(
|
||||
name: "delete",
|
||||
callback: (file: TAbstractFile) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(
|
||||
name: "rename",
|
||||
callback: (file: TAbstractFile, oldPath: string) => unknown,
|
||||
ctx?: unknown
|
||||
): EventRef;
|
||||
on(name: "closed", callback: () => unknown, ctx?: unknown): EventRef;
|
||||
on(name: unknown, callback: unknown, ctx?: unknown): EventRef {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
offref(ref: EventRef): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
append(
|
||||
file: TFile,
|
||||
data: string,
|
||||
options?: DataWriteOptions
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
appendBinary(
|
||||
file: TFile,
|
||||
data: ArrayBuffer,
|
||||
options?: DataWriteOptions
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
createBinary(
|
||||
path: string,
|
||||
data: ArrayBuffer,
|
||||
options?: DataWriteOptions
|
||||
): Promise<TFile> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
readBinary(file: TFile): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
modifyBinary(
|
||||
file: TFile,
|
||||
data: ArrayBuffer,
|
||||
options?: DataWriteOptions
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getResourcePath(file: TFile): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getResourcePath(file: TFile): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
114
test_helpers/utils.test.ts
Normal file
114
test_helpers/utils.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import {
|
||||
toRomanUpper,
|
||||
toRomanLower,
|
||||
toAlphUpper,
|
||||
toAlphLower,
|
||||
getEqNumberPrefix,
|
||||
CONVERTER
|
||||
} from '../src/utils/format';
|
||||
import {
|
||||
trimMathText,
|
||||
parseMarkdownComment,
|
||||
parseYamlLike,
|
||||
getCalloutPrefix,
|
||||
isStructuralCalloutLine
|
||||
} from '../src/utils/parse';
|
||||
|
||||
import { splitIntoLines, insertAt } from '../src/utils/general';
|
||||
import { App, TFile } from 'obsidian';
|
||||
|
||||
describe('format.ts tests', () => {
|
||||
it('Roman numerals conversion', () => {
|
||||
expect(toRomanUpper(1)).toBe('I');
|
||||
expect(toRomanUpper(3)).toBe('III');
|
||||
expect(toRomanUpper(4)).toBe('IV');
|
||||
expect(toRomanUpper(9)).toBe('IX');
|
||||
expect(toRomanUpper(10)).toBe('X');
|
||||
expect(toRomanUpper(39)).toBe('XXXIX');
|
||||
expect(toRomanUpper(40)).toBe('XL');
|
||||
expect(toRomanUpper(50)).toBe('L');
|
||||
expect(toRomanUpper(90)).toBe('XC');
|
||||
expect(toRomanUpper(100)).toBe('C');
|
||||
expect(toRomanUpper(400)).toBe('CD');
|
||||
expect(toRomanUpper(500)).toBe('D');
|
||||
expect(toRomanUpper(900)).toBe('CM');
|
||||
expect(toRomanUpper(1000)).toBe('M');
|
||||
expect(toRomanUpper(1994)).toBe('MCMXCIV');
|
||||
|
||||
expect(toRomanLower(1994)).toBe('mcmxciv');
|
||||
});
|
||||
|
||||
it('Alphabetic numerals conversion', () => {
|
||||
expect(toAlphUpper(1)).toBe('A');
|
||||
expect(toAlphUpper(26)).toBe('Z');
|
||||
expect(toAlphUpper(27)).toBe('BA');
|
||||
expect(toAlphUpper(28)).toBe('BB');
|
||||
expect(toAlphLower(1)).toBe('a');
|
||||
expect(toAlphLower(27)).toBe('ba');
|
||||
});
|
||||
|
||||
it('CONVERTER mapping', () => {
|
||||
expect(CONVERTER.arabic(123)).toBe('123');
|
||||
expect(CONVERTER.alph(1)).toBe('a');
|
||||
expect(CONVERTER.Alph(1)).toBe('A');
|
||||
expect(CONVERTER.roman(10)).toBe('x');
|
||||
expect(CONVERTER.Roman(10)).toBe('X');
|
||||
});
|
||||
|
||||
it('getEqNumberPrefix', () => {
|
||||
const dummyApp = {} as App;
|
||||
const dummyFile = {} as TFile;
|
||||
const settings = {
|
||||
eqNumberPrefix: 'Prefix-'
|
||||
} as any;
|
||||
expect(getEqNumberPrefix(dummyApp, dummyFile, settings)).toBe('Prefix-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse.ts tests', () => {
|
||||
it('trimMathText', () => {
|
||||
expect(trimMathText('$$ E = mc^2 $$')).toBe('E = mc^2');
|
||||
expect(trimMathText('$$\nE = mc^2\n$$')).toBe('E = mc^2');
|
||||
expect(trimMathText('no math block')).toBe('');
|
||||
});
|
||||
|
||||
it('parseMarkdownComment', () => {
|
||||
const text = 'Some text\n%% comment line 1\ncomment line 2 %%\nOther text %% single comment %%';
|
||||
const comments = parseMarkdownComment(text);
|
||||
expect(comments).toEqual(['comment line 1', 'comment line 2', 'single comment']);
|
||||
});
|
||||
|
||||
it('parseYamlLike', () => {
|
||||
expect(parseYamlLike('key: value')).toEqual({ key: 'value' });
|
||||
expect(parseYamlLike(' key : value ')).toEqual({ key: 'value' });
|
||||
expect(parseYamlLike('no colon')).toBeNull();
|
||||
expect(parseYamlLike('key: value: extra')).toEqual({ key: 'value: extra' });
|
||||
});
|
||||
|
||||
it('getCalloutPrefix', () => {
|
||||
expect(getCalloutPrefix('> hello')).toBe('> ');
|
||||
expect(getCalloutPrefix('>> hello')).toBe('>> ');
|
||||
expect(getCalloutPrefix(' > > hello')).toBe(' > > ');
|
||||
expect(getCalloutPrefix('no callout')).toBe('');
|
||||
});
|
||||
|
||||
it('isStructuralCalloutLine', () => {
|
||||
expect(isStructuralCalloutLine('>')).toBe(true);
|
||||
expect(isStructuralCalloutLine('> > ')).toBe(true);
|
||||
expect(isStructuralCalloutLine(' > >')).toBe(true);
|
||||
expect(isStructuralCalloutLine('> text')).toBe(false);
|
||||
expect(isStructuralCalloutLine('text')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general.ts tests', () => {
|
||||
it('splitIntoLines', () => {
|
||||
expect(splitIntoLines('line1\nline2\r\nline3')).toEqual(['line1', 'line2', 'line3']);
|
||||
});
|
||||
|
||||
it('insertAt', () => {
|
||||
const arr = [1, 2, 3];
|
||||
insertAt(arr, 99, 1);
|
||||
expect(arr).toEqual([1, 99, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,39 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"rootDir": ".",
|
||||
"baseUrl": "./src",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"ES2021.String",
|
||||
"DOM.Iterable",
|
||||
"es2022"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.d.ts",
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/legacy/*.ts",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"main.js",
|
||||
"test_helpers",
|
||||
"__mocks__"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"rootDir": ".",
|
||||
"baseUrl": "./src",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7", "ES2021.String", "DOM.Iterable", "es2022"]
|
||||
},
|
||||
"include": ["**/*.d.ts", "**/*.ts"],
|
||||
"exclude": [
|
||||
"src/legacy/*.ts",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"main.js",
|
||||
"test_helpers",
|
||||
"__mocks__"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"0.3.0": "1.3.5",
|
||||
"0.3.1": "1.3.5",
|
||||
"0.3.2": "1.3.5",
|
||||
"2.2.3": "1.3.5",
|
||||
"2.2.4": "1.3.5"
|
||||
}
|
||||
"0.3.0": "1.3.5",
|
||||
"0.3.1": "1.3.5",
|
||||
"0.3.2": "1.3.5",
|
||||
"2.2.3": "1.3.5",
|
||||
"2.2.4": "1.3.5"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue