mirror of
https://github.com/gapmiss/badges.git
synced 2026-07-22 08:00:30 +00:00
- Add eslint-plugin-obsidianmd with typescript-eslint type-checked rules - Fix TypeScript: remove any types, unused vars, console.log statements - Fix DOM: use createSpan() instead of document.createElement - Fix CSS: remove !important declarations - Replace builtin-modules with Node's module.builtinModules - Pin @codemirror dependencies to exact versions - Add pnpm lockfile for reproducible builds - Add GitHub workflow for artifact attestation - Add release script Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
217 lines
6.3 KiB
TypeScript
217 lines
6.3 KiB
TypeScript
import { Plugin, MarkdownPostProcessor, MarkdownPostProcessorContext, setIcon, editorLivePreviewField } from 'obsidian'
|
|
import { RangeSetBuilder } from "@codemirror/state"
|
|
import { ViewPlugin, WidgetType, EditorView, ViewUpdate, Decoration, DecorationSet } from '@codemirror/view'
|
|
import { BADGE_TYPES } from './constants';
|
|
|
|
const REGEXP = /(`\[!!([^\]]*)\]`)/gm;
|
|
const TAGS = 'code'
|
|
|
|
export default class BadgesPlugin extends Plugin {
|
|
public postprocessor: MarkdownPostProcessor = (el: HTMLElement, _ctx: MarkdownPostProcessorContext) => {
|
|
const blockToReplace = el.querySelectorAll(TAGS)
|
|
if (blockToReplace.length === 0) return
|
|
|
|
function replace(node: Node) {
|
|
const childrenToReplace: Text[] = []
|
|
node.childNodes.forEach(child => {
|
|
if (child.nodeType === 3) {
|
|
childrenToReplace.push(child as Text)
|
|
}
|
|
})
|
|
childrenToReplace.forEach((child) => {
|
|
child.replaceWith(child);
|
|
})
|
|
}
|
|
|
|
blockToReplace.forEach(block => {
|
|
replace(block)
|
|
})
|
|
}
|
|
|
|
async onload() {
|
|
this.registerMarkdownPostProcessor(
|
|
buildPostProcessor()
|
|
);
|
|
this.registerEditorExtension(viewPlugin)
|
|
}
|
|
|
|
onunload() {
|
|
}
|
|
}
|
|
|
|
function buildPostProcessor(): MarkdownPostProcessor {
|
|
return (el) => {
|
|
el.findAll("code").forEach(
|
|
(code) => {
|
|
let text:string|undefined = code.innerText.trim();
|
|
if (text !== undefined && text.startsWith('[!!') && text.endsWith(']')) {
|
|
let newEl = buildBadge(text);
|
|
if (newEl !== undefined) {
|
|
code.replaceWith(newEl);
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
class BadgeWidget extends WidgetType {
|
|
constructor(readonly badge: string[]) {
|
|
super()
|
|
}
|
|
|
|
toDOM(_view: EditorView): HTMLElement {
|
|
let text:string = this.badge[0].substring(1).substring(this.badge[0].length-2,0);
|
|
return buildBadge(text);
|
|
}
|
|
}
|
|
|
|
const viewPlugin = ViewPlugin.fromClass(class {
|
|
decorations: DecorationSet;
|
|
|
|
constructor(view: EditorView) {
|
|
this.decorations = this.buildDecorations(view);
|
|
}
|
|
|
|
update(update: ViewUpdate) {
|
|
this.decorations = this.buildDecorations(update.view);
|
|
}
|
|
|
|
destroy() { }
|
|
|
|
buildDecorations(view: EditorView): DecorationSet {
|
|
if (!view.state.field(editorLivePreviewField)) {
|
|
return Decoration.none;
|
|
}
|
|
let builder = new RangeSetBuilder<Decoration>();
|
|
let lines: number[] = [];
|
|
if (view.state.doc.length > 0) {
|
|
lines = Array.from(
|
|
{ length: view.state.doc.lines },
|
|
(_, i) => i + 1,
|
|
);
|
|
}
|
|
|
|
const currentSelections = [...view.state.selection.ranges];
|
|
|
|
for (let n of lines) {
|
|
const line = view.state.doc.line(n);
|
|
const startOfLine = line.from;
|
|
const endOfLine = line.to;
|
|
|
|
let _currentLine = false;
|
|
currentSelections.forEach((r) => {
|
|
if (r.to >= startOfLine && r.from <= endOfLine) {
|
|
_currentLine = true;
|
|
return;
|
|
}
|
|
});
|
|
|
|
let matches = Array.from(line.text.matchAll(REGEXP))
|
|
for (const match of matches) {
|
|
let add = true
|
|
const from = match.index != undefined ? match.index + line.from : -1
|
|
const to = from + match[0].length
|
|
if ((to-from) === 6) {
|
|
add = false
|
|
}
|
|
currentSelections.forEach((r) => {
|
|
if (r.to >= from && r.from <= to) {
|
|
add = false
|
|
}
|
|
})
|
|
if (add) {
|
|
builder.add(from, to, Decoration.widget({ widget: new BadgeWidget(match) }))
|
|
}
|
|
}
|
|
}
|
|
return builder.finish();
|
|
}
|
|
}, {
|
|
decorations: (v) => v.decorations,
|
|
})
|
|
|
|
function buildBadge(text: string): HTMLSpanElement {
|
|
const newEl = createSpan();
|
|
const iconEl = createSpan();
|
|
const titleEl = createSpan();
|
|
const textEl = createSpan();
|
|
let attrType = "";
|
|
const part = text.substring(2);
|
|
const content = part.substring(part.length-1,1).trim();
|
|
if (!content.length) {
|
|
newEl.setText("Badges syntax error");
|
|
return newEl;
|
|
}
|
|
const parts = content.split(':');
|
|
if (parts.length < 2) {
|
|
newEl.setText("❌ Badges syntax error");
|
|
newEl.setAttr("style", "color:var(--text-error)")
|
|
return newEl;
|
|
}
|
|
let badgeType = parts[0].trim();
|
|
const extras = badgeType.split("|");
|
|
const hasExtra = extras.length > 1;
|
|
const badgeContent = parts[1].trim();
|
|
if (extras.length == 3) {
|
|
iconEl.addClass("inline-badge-icon");
|
|
attrType = 'customized';
|
|
setIcon(iconEl, extras[1]);
|
|
iconEl.setAttr("aria-label", extras[2]);
|
|
const details = parts[1].split("|");
|
|
const title = details[0].trim();
|
|
titleEl.addClass("inline-badge-title-inner");
|
|
titleEl.setText(title);
|
|
newEl.addClass('inline-badge');
|
|
newEl.setAttr("data-inline-badge", attrType.toLowerCase());
|
|
let color = 'currentColor';
|
|
if (details[1]) {
|
|
color = details[1].trim();
|
|
}
|
|
newEl.setAttr("style", "--customize-badge-color: "+color+";");
|
|
newEl.appendChild(iconEl);
|
|
if (textEl.getText() != "") {
|
|
newEl.appendChild(textEl);
|
|
}
|
|
newEl.appendChild(titleEl);
|
|
attrType = extras.join("|");
|
|
} else {
|
|
if (hasExtra) {
|
|
if (extras[1].startsWith('ghb>') || extras[1].startsWith('ghs>')) {
|
|
const ghType = extras[1].split('>')[1].trim();
|
|
setIcon(iconEl, "github");
|
|
iconEl.addClass("inline-badge-icon");
|
|
iconEl.setAttr("aria-label", "Github");
|
|
textEl.addClass("gh-type");
|
|
textEl.setText(ghType);
|
|
iconEl.appendChild(textEl);
|
|
attrType = (extras[1].startsWith('ghb>')) ? 'github' : 'github-success';
|
|
badgeType = (extras[1].startsWith('ghb>')) ? 'github' : 'github-success';
|
|
} else {
|
|
iconEl.addClass("inline-badge-extra");
|
|
iconEl.setText(badgeType.split("|")[1].trim());
|
|
attrType = 'text';
|
|
badgeType = 'text';
|
|
}
|
|
} else {
|
|
iconEl.addClass("inline-badge-icon");
|
|
attrType = badgeType.trim();
|
|
BADGE_TYPES.forEach((el) => {
|
|
if (el[0] === badgeType.toLowerCase() && el[2].length > 0) {
|
|
setIcon(iconEl, el[2]);
|
|
iconEl.setAttr("aria-label", badgeType.trim());
|
|
}
|
|
});
|
|
}
|
|
titleEl.addClass("inline-badge-title-inner");
|
|
titleEl.setText(badgeContent);
|
|
newEl.addClass('inline-badge');
|
|
newEl.setAttr("data-inline-badge", attrType.toLowerCase());
|
|
newEl.appendChild(iconEl);
|
|
if (textEl.getText() != "") {
|
|
newEl.appendChild(textEl);
|
|
}
|
|
newEl.appendChild(titleEl);
|
|
}
|
|
return newEl;
|
|
}
|