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