mirror of
https://github.com/necauqua/obsidian-page-properties.git
synced 2026-07-22 07:30:24 +00:00
Initial commit, version 1.0.0
This commit is contained in:
commit
f1633580c3
20 changed files with 2989 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
main.js
|
||||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"env": { "node": true },
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Don't include the compiled main.js file in the repo.
|
||||||
|
main.js
|
||||||
|
|
||||||
|
# Exclude sourcemaps
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# Plugin settings
|
||||||
|
data.json
|
||||||
|
|
||||||
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Exclude the direnv cache
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# In case you ever run nix build
|
||||||
|
result
|
||||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
tag-version-prefix=""
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2023 Anton Bulakh <self@necauqua.dev>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Obsidian Page Properties
|
||||||
|
|
||||||
|
This is a plugin for [Obsidian](https://obsidian.md) which adds page properties similar to those present in Logseq.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
The main two things it does are:
|
||||||
|
- Adds pretty tag-like styles for full-line inline fields from [Dataview](https://github.com/blacksmithgu/obsidian-dataview) - note that while Dataview is not a dependency they're not that useful without it.
|
||||||
|
- Makes the field name into an inner link - a cute little feature from Logseq.
|
||||||
|
|
||||||
|
Another couple of features, that are *missing* in Logseq are:
|
||||||
|
- For certain fields the value is converted into a link by a specified pattern - this is configurable.
|
||||||
|
- This is useful when the tag already describes what the host should be, for example a relevant GitHub repository, or a wiki page - instead of the full link you only set the username/repository part or the wiki page name.
|
||||||
|
- Works really well with the [Surfing](https://obsidian.md/plugins?id=surfing) plugin :)
|
||||||
|
- Some fields can be hidden from the reader view - also configurable.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
- Currently awaiting approval on the [Obsidian Plugin Marketplace](https://obsidian.md/plugins?id=page-properties), so use one of the methods below.
|
||||||
|
- You can use the [Brat](https://github.com/TfTHacker/obsidian42-brat) plugin.
|
||||||
|
- You can download the `main.js`, `styles.css` and `manifest.json` files manually from the [releases](https://github.com/necauqua/obsidian-page-properties/releases) page and put them into the `$VAULT/.obsidian/plugins/page-properties` folder.
|
||||||
|
- For Nix users, you can do this too:
|
||||||
|
```bash
|
||||||
|
nix profile install github:necauqua/obsidian-page-properties
|
||||||
|
ln -s ~/.nix-profile/share/obsidian/plugins/page-properties $VAULT/.obsidian/plugins/page-properties
|
||||||
|
```
|
||||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import process from "process";
|
||||||
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
`/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
banner: {
|
||||||
|
js: banner,
|
||||||
|
},
|
||||||
|
entryPoints: ["src/main.ts"],
|
||||||
|
bundle: true,
|
||||||
|
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",
|
||||||
|
...builtins],
|
||||||
|
format: "cjs",
|
||||||
|
target: "es2018",
|
||||||
|
logLevel: "info",
|
||||||
|
sourcemap: prod ? false : "inline",
|
||||||
|
treeShaking: true,
|
||||||
|
outfile: "main.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prod) {
|
||||||
|
await context.rebuild();
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
await context.watch();
|
||||||
|
}
|
||||||
40
flake.lock
Normal file
40
flake.lock
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1678901627,
|
||||||
|
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "flake-utils",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1679281263,
|
||||||
|
"narHash": "sha256-neMref1GTruSLt1jBgAw+lvGsZj8arQYfdxvSi5yp4Q=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "8276a165b9fa3db1a7a4f29ee29b680e0799b9dc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
24
flake.nix
Normal file
24
flake.nix
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages."${system}";
|
||||||
|
in {
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [ pkgs.nodejs ];
|
||||||
|
};
|
||||||
|
packages.default = pkgs.buildNpmPackage {
|
||||||
|
name = "obsidian-page-properties";
|
||||||
|
src = ./.;
|
||||||
|
packageJson = ./package.json;
|
||||||
|
packageLockJson = ./package-lock.json;
|
||||||
|
npmDepsHash = "sha256-bMzD0478ysBjLB97CRdM9cBHonHTVrlONR+X3h47p4c=";
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/share/obsidian/plugins/page-properties
|
||||||
|
npm run build
|
||||||
|
cp main.js styles.css manifest.json $out/share/obsidian/plugins/page-properties
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
15
manifest.json
Normal file
15
manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"id": "page-properties",
|
||||||
|
"name": "Page Properties",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Render page properties similar to Logseq",
|
||||||
|
"author": "Anton Bulakh <self@necauqua.dev>",
|
||||||
|
"authorUrl": "https://necauqua.dev",
|
||||||
|
"fundingUrl": {
|
||||||
|
"Patreon": "https://www.patreon.com/necauqua",
|
||||||
|
"Ko-Fi": "https://ko-fi.com/necauqua",
|
||||||
|
"Buy Me a Coffee": "https://buymeacoffee.com/necauqua"
|
||||||
|
},
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
2144
package-lock.json
generated
Normal file
2144
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "obsidian-page-properties",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "This plugin adds page properties similar to Logseq",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||||
|
"prettier": "prettier --write src"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
|
"builtin-modules": "3.3.0",
|
||||||
|
"esbuild": "0.17.3",
|
||||||
|
"eslint": "^8.36.0",
|
||||||
|
"obsidian": "^0.15.4",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/main.ts
Normal file
155
src/main.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { App, Plugin, PluginSettingTab, Setting } from 'obsidian'
|
||||||
|
import { createViewPlugin, createMarkdownPostProcessor } from './render'
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
fieldsAreInnerLinks: true,
|
||||||
|
autolinkPatterns: [
|
||||||
|
{ field: 'github', prefix: 'https://github.com/{0}' },
|
||||||
|
{ field: 'wiki', prefix: 'https://wikipedia.org/wiki/{0}' },
|
||||||
|
{ field: 'twitter', prefix: 'https://twitter.com/{0}' },
|
||||||
|
],
|
||||||
|
hideInReaderMode: [
|
||||||
|
'public'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PagePropsSettings = typeof DEFAULT_SETTINGS
|
||||||
|
type AutolinkPatterns = Record<string, string | undefined>
|
||||||
|
|
||||||
|
export default class PagePropsPlugin extends Plugin {
|
||||||
|
|
||||||
|
settings: PagePropsSettings
|
||||||
|
autolinkPatterns: AutolinkPatterns
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
|
||||||
|
this.refreshAutolinkPatterns()
|
||||||
|
|
||||||
|
this.registerEditorExtension([createViewPlugin(this)])
|
||||||
|
this.registerMarkdownPostProcessor(createMarkdownPostProcessor(this), -100)
|
||||||
|
this.addSettingTab(new Settings(this.app, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshAutolinkPatterns() {
|
||||||
|
this.autolinkPatterns = {}
|
||||||
|
for (const entry of this.settings.autolinkPatterns) {
|
||||||
|
this.autolinkPatterns[entry.field] = entry.prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
this.refreshAutolinkPatterns()
|
||||||
|
await this.saveData(this.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Settings extends PluginSettingTab {
|
||||||
|
|
||||||
|
constructor(app: App, readonly plugin: PagePropsPlugin) {
|
||||||
|
super(app, plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this
|
||||||
|
containerEl.empty()
|
||||||
|
|
||||||
|
containerEl.createEl('h2', { text: 'General' })
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Page prop names are inner links')
|
||||||
|
.setDesc('A feature from Logseq, you can disable that here if you want.')
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.fieldsAreInnerLinks)
|
||||||
|
.onChange(async value => {
|
||||||
|
this.plugin.settings.fieldsAreInnerLinks = value
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { autolinkPatterns, hideInReaderMode } = this.plugin.settings
|
||||||
|
|
||||||
|
containerEl.createEl('h2', { text: 'Fields to hide in reader view' })
|
||||||
|
|
||||||
|
for (let index = 0; index < hideInReaderMode.length; index++) {
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Hidden field #' + (index + 1))
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Field that will be hidden')
|
||||||
|
.setValue(hideInReaderMode[index])
|
||||||
|
.onChange(async value => {
|
||||||
|
hideInReaderMode[index] = value
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
.addButton(button => button
|
||||||
|
.setButtonText('-')
|
||||||
|
.onClick(async () => {
|
||||||
|
hideInReaderMode.splice(index, 1)
|
||||||
|
this.display() // reload the view
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
new Setting(containerEl)
|
||||||
|
.addButton(button => button
|
||||||
|
.setWarning()
|
||||||
|
.setButtonText('Reset')
|
||||||
|
.onClick(async () => {
|
||||||
|
this.plugin.settings.hideInReaderMode = [...DEFAULT_SETTINGS.hideInReaderMode]
|
||||||
|
this.display()
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
.addButton(button => button
|
||||||
|
.setButtonText('+')
|
||||||
|
.onClick(async () => {
|
||||||
|
hideInReaderMode.push('')
|
||||||
|
this.display()
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
containerEl.createEl('h2', { text: 'Autolink Patterns' })
|
||||||
|
|
||||||
|
for (let index = 0; index < autolinkPatterns.length; index++) {
|
||||||
|
const entry = autolinkPatterns[index]
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Pattern #' + (index + 1))
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Field for the link pattern')
|
||||||
|
.setValue(entry.field)
|
||||||
|
.onChange(async value => {
|
||||||
|
entry.field = value
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('The link pattern')
|
||||||
|
.setValue(entry.prefix)
|
||||||
|
.onChange(async value => {
|
||||||
|
entry.prefix = value
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
.addButton(button => button
|
||||||
|
.setButtonText('-')
|
||||||
|
.onClick(async () => {
|
||||||
|
autolinkPatterns.splice(index, 1)
|
||||||
|
this.display()
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
new Setting(containerEl)
|
||||||
|
.addButton(button => button
|
||||||
|
.setWarning()
|
||||||
|
.setButtonText('Reset')
|
||||||
|
.onClick(async () => {
|
||||||
|
this.plugin.settings.autolinkPatterns = [...DEFAULT_SETTINGS.autolinkPatterns]
|
||||||
|
this.display()
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}))
|
||||||
|
.addButton(button => button
|
||||||
|
.setButtonText('+')
|
||||||
|
.onClick(async () => {
|
||||||
|
autolinkPatterns.push({ field: '', prefix: '' })
|
||||||
|
this.display()
|
||||||
|
await this.plugin.saveSettings()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
297
src/render.ts
Normal file
297
src/render.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'
|
||||||
|
import { Range, Extension } from '@codemirror/state'
|
||||||
|
import PagePropsPlugin from './main'
|
||||||
|
|
||||||
|
|
||||||
|
const fieldName = /([\p{L}\p{Extended_Pictographic}][0-9\p{L}\p{Extended_Pictographic}\s_/-]*)/u
|
||||||
|
|
||||||
|
function decoratedRegex(dec: string): RegExp {
|
||||||
|
return new RegExp(`(^\\s*)${dec}${fieldName.source}${dec}(::\\s*)(.*)$`, 'u')
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = decoratedRegex('')
|
||||||
|
const specialRegex1 = decoratedRegex('\\*\\*')
|
||||||
|
const specialRegex2 = decoratedRegex('\\*')
|
||||||
|
const specialRegex3 = decoratedRegex('`')
|
||||||
|
const specialRegex4 = decoratedRegex('_')
|
||||||
|
|
||||||
|
interface ParsedField {
|
||||||
|
prespace: string,
|
||||||
|
field: string,
|
||||||
|
fieldLen: number,
|
||||||
|
separator: string,
|
||||||
|
content: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseField(line: string): ParsedField | null {
|
||||||
|
let res = regex.exec(line)
|
||||||
|
|
||||||
|
// wtf is this lol
|
||||||
|
let extraLen = 0
|
||||||
|
if (!res) {
|
||||||
|
extraLen = 4;
|
||||||
|
res = specialRegex1.exec(line)
|
||||||
|
if (!res) {
|
||||||
|
extraLen = 2;
|
||||||
|
res = specialRegex2.exec(line)
|
||||||
|
if (!res) {
|
||||||
|
res = specialRegex3.exec(line)
|
||||||
|
if (!res) {
|
||||||
|
res = specialRegex4.exec(line)
|
||||||
|
if (!res) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [, prespace, field, separator, content] = res
|
||||||
|
return {
|
||||||
|
prespace,
|
||||||
|
field,
|
||||||
|
fieldLen: field.length + extraLen,
|
||||||
|
separator,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(plugin: PagePropsPlugin, field: string, separator: string, content: Node[], fieldNode?: Node): HTMLElement {
|
||||||
|
const span = createSpan()
|
||||||
|
|
||||||
|
const text = fieldNode ? undefined : field
|
||||||
|
const fieldEl = plugin.settings.fieldsAreInnerLinks ?
|
||||||
|
createEl('a', {
|
||||||
|
cls: `internal-link page-prop page-prop--${field} page-prop-field`,
|
||||||
|
text,
|
||||||
|
href: field,
|
||||||
|
attr: { 'data-href': field, spellcheck: 'false', target: '_blank', rel: 'noopener' },
|
||||||
|
parent: span,
|
||||||
|
}) :
|
||||||
|
createSpan({
|
||||||
|
cls: `page-prop page-prop--${field} page-prop-field`,
|
||||||
|
text,
|
||||||
|
attr: { spellcheck: 'false' },
|
||||||
|
parent: span,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fieldNode) {
|
||||||
|
fieldEl.appendChild(fieldNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
createSpan({
|
||||||
|
cls: `page-prop page-prop--${field} page-prop-separator`,
|
||||||
|
text: separator,
|
||||||
|
attr: { spellcheck: 'false' },
|
||||||
|
parent: span,
|
||||||
|
})
|
||||||
|
if (content.length == 1 && content[0].nodeType == Node.TEXT_NODE) {
|
||||||
|
const pattern = plugin.autolinkPatterns[field]
|
||||||
|
if (pattern) {
|
||||||
|
const text = content[0].textContent || ''
|
||||||
|
createEl('a', {
|
||||||
|
cls: `external-link page-prop page-prop--${field} page-prop-content`,
|
||||||
|
text,
|
||||||
|
href: pattern.format(encodeURI(text)),
|
||||||
|
attr: { spellcheck: 'false' },
|
||||||
|
parent: span,
|
||||||
|
})
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const holder = createSpan({
|
||||||
|
cls: `page-prop page-prop--${field} page-prop-content`,
|
||||||
|
attr: { spellcheck: 'false' },
|
||||||
|
parent: span,
|
||||||
|
})
|
||||||
|
for (const node of content) {
|
||||||
|
holder.appendChild(node)
|
||||||
|
}
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkdownPostProcessor(plugin: PagePropsPlugin): (el: HTMLElement) => void {
|
||||||
|
return el => {
|
||||||
|
if (el.childElementCount != 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = el.firstChild
|
||||||
|
if (!p || p.nodeName != 'P') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nodes = p.childNodes
|
||||||
|
|
||||||
|
const lines: Node[][] = []
|
||||||
|
let currentLine: Node[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i]
|
||||||
|
if (node.nodeName == 'BR') {
|
||||||
|
lines.push(currentLine)
|
||||||
|
currentLine = []
|
||||||
|
} else if (node.nodeType != Node.TEXT_NODE || node.textContent != '\n') {
|
||||||
|
currentLine.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(currentLine)
|
||||||
|
|
||||||
|
for (const lineNodes of lines) {
|
||||||
|
if (lineNodes.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const [first] = lineNodes
|
||||||
|
|
||||||
|
// the common case
|
||||||
|
if (first.nodeType == Node.TEXT_NODE) {
|
||||||
|
const res = parseField(first.textContent || '')
|
||||||
|
if (!res) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { prespace, field, separator, content: contentTextPart } = res
|
||||||
|
if (contentTextPart.length == 0 && lineNodes.length == 1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prespace.length != 0) {
|
||||||
|
p.insertBefore(document.createTextNode(prespace), first)
|
||||||
|
}
|
||||||
|
if (!plugin.settings.hideInReaderMode.contains(field)) {
|
||||||
|
const contentNodes = [document.createTextNode(contentTextPart), ...lineNodes.slice(1)]
|
||||||
|
p.insertBefore(render(plugin, field, separator, contentNodes), first)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.removeChild(first)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// less common case, we allow the key to be (fully) bold, italic or monospace:
|
||||||
|
if (lineNodes.length == 1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// next node must be at least "::"
|
||||||
|
const [, second] = lineNodes
|
||||||
|
if (second.nodeType != Node.TEXT_NODE) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let preContent = second.textContent || ''
|
||||||
|
if (!preContent.startsWith('::')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
preContent = preContent.substring(2)
|
||||||
|
|
||||||
|
// meh, extract the field I guess
|
||||||
|
const res = parseField((first.textContent || '') + '::')
|
||||||
|
if (!res) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { prespace, field, separator } = res
|
||||||
|
if (preContent.trim().length == 0 && lineNodes.length == 2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prespace.length != 0) {
|
||||||
|
p.insertBefore(document.createTextNode(prespace), first)
|
||||||
|
}
|
||||||
|
if (!plugin.settings.hideInReaderMode.contains(field)) {
|
||||||
|
const contentNodes = preContent.length != 0 ?
|
||||||
|
[document.createTextNode(preContent), ...lineNodes.slice(2)] :
|
||||||
|
lineNodes.slice(2)
|
||||||
|
const rendered = render(plugin, field, separator, contentNodes, first)
|
||||||
|
p.insertBefore(rendered, second)
|
||||||
|
}
|
||||||
|
p.removeChild(second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorate(plugin: PagePropsPlugin, view: EditorView): DecorationSet {
|
||||||
|
const { doc } = view.state
|
||||||
|
|
||||||
|
const widgets: Range<Decoration>[] = []
|
||||||
|
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
for (let i = doc.lineAt(from).number; i <= doc.lineAt(to).number; i++) {
|
||||||
|
const line = doc.line(i)
|
||||||
|
|
||||||
|
if (line.text.trim().length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = parseField(line.text)
|
||||||
|
if (!res) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { prespace, field, fieldLen, separator, content } = res
|
||||||
|
|
||||||
|
if (content.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameStart = line.from + prespace.length
|
||||||
|
const nameEnd = nameStart + fieldLen
|
||||||
|
const contentStart = nameEnd + separator.length
|
||||||
|
const contentEnd = line.to
|
||||||
|
|
||||||
|
let extraClass = ''
|
||||||
|
if (plugin.settings.hideInReaderMode.contains(field)) {
|
||||||
|
extraClass = ' page-prop-hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.settings.fieldsAreInnerLinks) {
|
||||||
|
widgets.push(Decoration.mark({
|
||||||
|
tagName: 'a',
|
||||||
|
class: `internal-link page-prop${extraClass} page-prop--${field} page-prop-field`,
|
||||||
|
attributes: {
|
||||||
|
href: field,
|
||||||
|
spellcheck: 'false',
|
||||||
|
draggable: 'true'
|
||||||
|
},
|
||||||
|
}).range(nameStart, nameEnd))
|
||||||
|
} else {
|
||||||
|
widgets.push(Decoration.mark({
|
||||||
|
class: `page-prop${extraClass} page-prop--${field} page-prop-field`,
|
||||||
|
attributes: { spellcheck: 'false' },
|
||||||
|
}).range(nameStart, nameEnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets.push(Decoration.mark({
|
||||||
|
class: `page-prop${extraClass} page-prop--${field} page-prop-separator`,
|
||||||
|
attributes: { spellcheck: 'false' },
|
||||||
|
}).range(nameEnd, contentStart))
|
||||||
|
|
||||||
|
const cls = `page-prop${extraClass} page-prop--${field} page-prop-content`
|
||||||
|
|
||||||
|
const pattern = plugin.autolinkPatterns[field]
|
||||||
|
if (pattern) {
|
||||||
|
const href = pattern.format(encodeURI(content))
|
||||||
|
widgets.push(Decoration.mark({
|
||||||
|
tagName: 'a',
|
||||||
|
class: `external-link page-prop page-prop--${field} page-prop-content`,
|
||||||
|
attributes: { href, spellcheck: 'false' },
|
||||||
|
}).range(contentStart, contentEnd))
|
||||||
|
} else {
|
||||||
|
widgets.push(Decoration.mark({
|
||||||
|
class: cls,
|
||||||
|
attributes: { spellcheck: 'false' },
|
||||||
|
}).range(contentStart, contentEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Decoration.set(widgets)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createViewPlugin(plugin: PagePropsPlugin): Extension {
|
||||||
|
return ViewPlugin.fromClass(class {
|
||||||
|
decorations: DecorationSet
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = decorate(plugin, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||||
|
this.decorations = decorate(plugin, update.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { decorations: v => v.decorations })
|
||||||
|
}
|
||||||
88
styles.css
Normal file
88
styles.css
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
body {
|
||||||
|
--page-prop-color: var(--tag-color);
|
||||||
|
--page-prop-background: var(--tag-background);
|
||||||
|
--page-prop-border-width: var(--tag-border-width);
|
||||||
|
--page-prop-border-color: var(--tag-border-color);
|
||||||
|
--page-prop-size: var(--tag-size);
|
||||||
|
--page-prop-padding-x: var(--tag-padding-x);
|
||||||
|
--page-prop-padding-y: var(--tag-padding-y);
|
||||||
|
--page-prop-radius: var(--tag-radius);
|
||||||
|
|
||||||
|
--page-prop-hidden-color: hsl(254deg 80% 68% / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* basically a copy of .tag */
|
||||||
|
.page-prop {
|
||||||
|
color: var(--page-prop-color);
|
||||||
|
background-color: var(--page-prop-background);
|
||||||
|
border: var(--page-prop-border-width) solid var(--page-prop-border-color);
|
||||||
|
font-size: var(--page-prop-size);
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
padding-top: var(--page-prop-padding-y);
|
||||||
|
padding-bottom: var(--page-prop-padding-y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* instead of just .page-prop.page-prop-field and .page-prop.page-prop-content below */
|
||||||
|
/* we have to have a ton of weird matchers because obsidian cm6 decorations always */
|
||||||
|
/* have the highest (or lowest? anyway, it's wrong compared to mine and can't be changed) */
|
||||||
|
/* precedence for some reason */
|
||||||
|
|
||||||
|
:is(div,.markdown-rendered span) > .page-prop.page-prop-field:first-of-type,
|
||||||
|
:is(div,.markdown-rendered span) > *:first-of-type > .page-prop.page-prop-field:first-of-type,
|
||||||
|
:is(div,.markdown-rendered span) > *:empty + :is(.cm-hmd-internal-link,.cm-strong,.cm-em,.cm-highlight,.cm-strikethrough,.cm-comment,.cm-inline-code) > .page-prop.page-prop-field:first-of-type {
|
||||||
|
border-top-left-radius: var(--page-prop-radius);
|
||||||
|
border-bottom-left-radius: var(--page-prop-radius);
|
||||||
|
border-right: none;
|
||||||
|
border-left: var(--page-prop-border-width) solid var(--page-prop-border-color);
|
||||||
|
padding-left: var(--page-prop-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(div,.markdown-rendered span) > .page-prop.page-prop-content:last-of-type,
|
||||||
|
:is(div,.markdown-rendered span) > *:last-of-type > .page-prop.page-prop-content:last-of-type,
|
||||||
|
:is(div,.markdown-rendered span) > :is(.cm-hmd-internal-link,.cm-strong,.cm-em,.cm-highlight,.cm-strikethrough,.cm-comment,.cm-inline-code):has(+ *:empty) > .page-prop.page-prop-content:last-of-type {
|
||||||
|
border-top-right-radius: var(--page-prop-radius);
|
||||||
|
border-bottom-right-radius: var(--page-prop-radius);
|
||||||
|
border-left: none;
|
||||||
|
border-right: var(--page-prop-border-width) solid var(--page-prop-border-color);
|
||||||
|
padding-right: var(--page-prop-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bleaker color for the hidden props */
|
||||||
|
.page-prop.page-prop-hidden {
|
||||||
|
color: var(--page-prop-hidden-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make the field-links only underlined on hover */
|
||||||
|
.page-prop.page-prop-field.internal-link {
|
||||||
|
text-decoration-line: none;
|
||||||
|
}
|
||||||
|
.page-prop.page-prop-field.internal-link:hover {
|
||||||
|
text-decoration-line: var(--link-decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dont highlight field links as unresolved */
|
||||||
|
.markdown-rendered .page-prop.page-prop-field.internal-link.is-unresolved {
|
||||||
|
color: var(--page-prop-color);
|
||||||
|
opacity: inherit;
|
||||||
|
filter: inherit;
|
||||||
|
text-decoration-style: inherit;
|
||||||
|
text-decoration-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable the little icon, it's misplaced interacts weirdly with round */
|
||||||
|
/* corners (but the class is needed for it to be clickable in the editor) */
|
||||||
|
/* second match is for the reader mode */
|
||||||
|
.page-prop.page-prop-content.external-link,
|
||||||
|
.page-prop.page-prop-content > a.external-link {
|
||||||
|
background-image: none;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fixup if *someone* uses an inline code block as a field name */
|
||||||
|
.cm-s-obsidian span.cm-inline-code:has(.page-prop) {
|
||||||
|
font-size: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
|
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
|
const { minAppVersion } = manifest;
|
||||||
|
manifest.version = targetVersion;
|
||||||
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
|
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue