mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
refactor: highlight synced content instead of markers
When highlight mode is on, hide markers as before but wrap the synced content itself in a highlight decoration: - Inline sections: background + outline on text between start/end markers - Line sections: same treatment on text after the line marker - Block sections: left accent border + tinted background on each content line Reading mode wraps the same content in a jira-sync-content span. Uses color-mix() for theme-aware accent tinting.
This commit is contained in:
parent
f4732263f8
commit
1562f4ff52
3 changed files with 128 additions and 108 deletions
|
|
@ -25,123 +25,96 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
|
|||
passDecoration() {
|
||||
const mdView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!mdView) return false;
|
||||
const current_state = mdView.getState()
|
||||
const current_state = mdView.getState();
|
||||
return current_state.mode === 'source' && current_state.source;
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const isHighlight = plugin.settings.global.highlightSyncSections;
|
||||
const sel = view.state.selection;
|
||||
|
||||
for (let { from, to } of view.visibleRanges) {
|
||||
const allDecs: Array<{ from: number; to: number; dec: Decoration }> = [];
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
const text = view.state.doc.sliceString(from, to);
|
||||
const codeBlocks = this.findCodeBlocks(text, from);
|
||||
|
||||
const allDecorations: Array<{from: number, to: number, dec: Decoration}> = [];
|
||||
|
||||
// Collect marker decorations
|
||||
// Collect all jira-sync marker positions
|
||||
const markers: Array<{ start: number; end: number; name: string }> = [];
|
||||
for (const match of text.matchAll(/`(jira-sync-[^`]+)`/g)) {
|
||||
const start = from + match.index!;
|
||||
const end = start + match[0].length;
|
||||
|
||||
const end = start + match[0].length;
|
||||
if (this.isInsideCodeBlock(start, end, codeBlocks)) continue;
|
||||
markers.push({ start, end, name: match[1] });
|
||||
}
|
||||
|
||||
const sel = view.state.selection;
|
||||
// Always hide the markers themselves, show when cursor is inside
|
||||
for (const { start, end } of markers) {
|
||||
const isActive = sel.ranges.some(r => r.from <= end && r.to >= start);
|
||||
|
||||
let className: string;
|
||||
if (isHighlight) {
|
||||
className = isActive ? "jira-sync-marker jira-sync-active" : "jira-sync-marker";
|
||||
} else {
|
||||
className = isActive ? "jira-sync-hidden jira-sync-active" : "jira-sync-hidden";
|
||||
}
|
||||
|
||||
allDecorations.push({ from: start, to: end, dec: Decoration.mark({ class: className }) });
|
||||
const cls = isActive ? "jira-sync-hidden jira-sync-active" : "jira-sync-hidden";
|
||||
allDecs.push({ from: start, to: end, dec: Decoration.mark({ class: cls }) });
|
||||
}
|
||||
|
||||
// Collect block content line decorations when highlight is on
|
||||
if (isHighlight) {
|
||||
const blockContentLines = this.findBlockContentLines(view, text, from, codeBlocks);
|
||||
for (const linePos of blockContentLines) {
|
||||
allDecorations.push({ from: linePos, to: linePos, dec: Decoration.line({ class: "jira-sync-block-content" }) });
|
||||
if (!isHighlight) continue;
|
||||
|
||||
// Inline content: `jira-sync-inline-start-*` ... `jira-sync-end`
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const m = markers[i];
|
||||
if (!m.name.startsWith('jira-sync-inline-start-')) continue;
|
||||
const endMarker = markers.find((em, j) => j > i && em.name === 'jira-sync-end');
|
||||
if (!endMarker || endMarker.start <= m.end) continue;
|
||||
allDecs.push({ from: m.end, to: endMarker.start, dec: Decoration.mark({ class: "jira-sync-content" }) });
|
||||
}
|
||||
|
||||
// Line content: `jira-sync-line-*` followed by text to end of line
|
||||
for (const m of markers) {
|
||||
if (!m.name.startsWith('jira-sync-line-')) continue;
|
||||
const line = view.state.doc.lineAt(m.end);
|
||||
const nextOnLine = markers.find(nm => nm.start > m.end && nm.start <= line.to);
|
||||
const contentEnd = nextOnLine ? nextOnLine.start : line.to;
|
||||
if (contentEnd > m.end) {
|
||||
allDecs.push({ from: m.end, to: contentEnd, dec: Decoration.mark({ class: "jira-sync-content" }) });
|
||||
}
|
||||
}
|
||||
|
||||
// RangeSetBuilder requires ranges in sorted order
|
||||
allDecorations.sort((a, b) => a.from - b.from);
|
||||
for (const { from: f, to: t, dec } of allDecorations) {
|
||||
builder.add(f, t, dec);
|
||||
// Block content: lines between `jira-sync-block-start-*` and `jira-sync-end`
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const m = markers[i];
|
||||
if (!m.name.startsWith('jira-sync-block-start-')) continue;
|
||||
const endMarker = markers.find((em, j) => j > i && em.name === 'jira-sync-end');
|
||||
if (!endMarker) continue;
|
||||
const startLine = view.state.doc.lineAt(m.end);
|
||||
const endLine = view.state.doc.lineAt(endMarker.start);
|
||||
for (let lineNum = startLine.number + 1; lineNum < endLine.number; lineNum++) {
|
||||
if (lineNum > view.state.doc.lines) break;
|
||||
const line = view.state.doc.line(lineNum);
|
||||
allDecs.push({ from: line.from, to: line.from, dec: Decoration.line({ class: "jira-sync-block-content" }) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RangeSetBuilder requires ranges in ascending order of `from`, then `to`
|
||||
allDecs.sort((a, b) => a.from - b.from || a.to - b.to);
|
||||
for (const { from, to, dec } of allDecs) {
|
||||
builder.add(from, to, dec);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
findBlockContentLines(view: EditorView, text: string, offset: number, codeBlocks: Array<{from: number, to: number}>): number[] {
|
||||
const linePositions: number[] = [];
|
||||
const blockStartRegex = /`jira-sync-block-start-[^`]+`/g;
|
||||
const blockEndRegex = /`jira-sync-end`/g;
|
||||
|
||||
const starts: number[] = [];
|
||||
const ends: number[] = [];
|
||||
|
||||
for (const m of text.matchAll(blockStartRegex)) {
|
||||
const pos = offset + m.index! + m[0].length;
|
||||
if (!this.isInsideCodeBlock(offset + m.index!, pos, codeBlocks)) {
|
||||
starts.push(pos);
|
||||
}
|
||||
}
|
||||
for (const m of text.matchAll(blockEndRegex)) {
|
||||
const pos = offset + m.index!;
|
||||
if (!this.isInsideCodeBlock(pos, pos + m[0].length, codeBlocks)) {
|
||||
ends.push(pos);
|
||||
}
|
||||
}
|
||||
|
||||
for (const startPos of starts) {
|
||||
const matchingEnd = ends.find(e => e > startPos);
|
||||
if (matchingEnd === undefined) continue;
|
||||
|
||||
// Iterate over lines between the start marker line end and the end marker line start
|
||||
let lineStart = view.state.doc.lineAt(startPos).from;
|
||||
const endLine = view.state.doc.lineAt(matchingEnd).from;
|
||||
|
||||
// Move to line after the block-start marker
|
||||
const startLine = view.state.doc.lineAt(startPos);
|
||||
if (startLine.number < view.state.doc.lines) {
|
||||
lineStart = view.state.doc.line(startLine.number + 1).from;
|
||||
}
|
||||
|
||||
while (lineStart < endLine) {
|
||||
linePositions.push(lineStart);
|
||||
const line = view.state.doc.lineAt(lineStart);
|
||||
if (line.number >= view.state.doc.lines) break;
|
||||
lineStart = view.state.doc.line(line.number + 1).from;
|
||||
}
|
||||
}
|
||||
|
||||
return linePositions;
|
||||
}
|
||||
|
||||
findCodeBlocks(text: string, offset: number) {
|
||||
const blocks = [];
|
||||
const blocks: Array<{ from: number; to: number }> = [];
|
||||
const regex = /```[\s\S]*?```/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
blocks.push({
|
||||
from: offset + match.index,
|
||||
to: offset + match.index + match[0].length
|
||||
});
|
||||
blocks.push({ from: offset + match.index, to: offset + match.index + match[0].length });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
isInsideCodeBlock(start: number, end: number, codeBlocks: Array<{from: number, to: number}>) {
|
||||
return codeBlocks.some(block =>
|
||||
start >= block.from && end <= block.to
|
||||
);
|
||||
isInsideCodeBlock(start: number, end: number, codeBlocks: Array<{ from: number; to: number }>) {
|
||||
return codeBlocks.some(block => start >= block.from && end <= block.to);
|
||||
}
|
||||
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,73 @@
|
|||
import JiraPlugin from "../main";
|
||||
|
||||
// For Reading mode - targets <code> elements in <p> tags
|
||||
export function hideJiraPointersReading(plugin: JiraPlugin) {
|
||||
return function(element: HTMLElement, _: any) {
|
||||
const codeElements = element.querySelectorAll(':not(pre) > code');
|
||||
const isHighlight = plugin.settings.global.highlightSyncSections;
|
||||
const codeElements = Array.from(element.querySelectorAll(':not(pre) > code'))
|
||||
.filter(el => el.textContent?.startsWith('jira-sync-')) as HTMLElement[];
|
||||
|
||||
codeElements.forEach(codeEl => {
|
||||
for (const codeEl of codeElements) {
|
||||
const text = codeEl.textContent || '';
|
||||
codeEl.addClass('jira-sync-hidden');
|
||||
|
||||
if (text.startsWith('jira-sync-')) {
|
||||
if (plugin.settings.global.highlightSyncSections) {
|
||||
codeEl.addClass('jira-sync-marker');
|
||||
} else {
|
||||
codeEl.addClass('jira-sync-hidden');
|
||||
}
|
||||
if (!isHighlight) continue;
|
||||
|
||||
if (text.startsWith('jira-sync-inline-start-')) {
|
||||
wrapContentBetween(codeEl, 'jira-sync-end');
|
||||
} else if (text.startsWith('jira-sync-line-')) {
|
||||
wrapContentAfter(codeEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap sibling nodes between this element and the next jira-sync-end code element
|
||||
function wrapContentBetween(startEl: HTMLElement, endMarker: string) {
|
||||
const parent = startEl.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const nodesToWrap: Node[] = [];
|
||||
let node: Node | null = startEl.nextSibling;
|
||||
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
if (el.tagName === 'CODE' && (el.textContent || '').startsWith(endMarker)) break;
|
||||
}
|
||||
nodesToWrap.push(node);
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
||||
if (nodesToWrap.length === 0) return;
|
||||
const wrapper = createSpanWrapper(parent, nodesToWrap[0]);
|
||||
for (const n of nodesToWrap) wrapper.appendChild(n);
|
||||
}
|
||||
|
||||
// Wrap all sibling nodes after this element (until next jira-sync marker)
|
||||
function wrapContentAfter(lineEl: HTMLElement) {
|
||||
const parent = lineEl.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const nodesToWrap: Node[] = [];
|
||||
let node: Node | null = lineEl.nextSibling;
|
||||
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
if (el.tagName === 'CODE' && (el.textContent || '').startsWith('jira-sync-')) break;
|
||||
}
|
||||
nodesToWrap.push(node);
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
||||
if (nodesToWrap.length === 0) return;
|
||||
const wrapper = createSpanWrapper(parent, nodesToWrap[0]);
|
||||
for (const n of nodesToWrap) wrapper.appendChild(n);
|
||||
}
|
||||
|
||||
function createSpanWrapper(parent: HTMLElement, insertBefore: Node): HTMLElement {
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.addClass('jira-sync-content');
|
||||
parent.insertBefore(wrapper, insertBefore);
|
||||
return wrapper;
|
||||
}
|
||||
|
|
|
|||
24
styles.css
24
styles.css
|
|
@ -953,26 +953,20 @@ code.hljs {
|
|||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Highlighted sync section markers */
|
||||
.jira-sync-marker {
|
||||
background: var(--tag-background);
|
||||
color: var(--tag-color);
|
||||
border-radius: var(--tag-radius, 4px);
|
||||
padding: var(--tag-padding-y, 1px) var(--tag-padding-x, 6px);
|
||||
font-size: var(--tag-size, 0.8em);
|
||||
font-weight: 500;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.jira-sync-marker.jira-sync-active {
|
||||
outline: 1px solid var(--interactive-accent);
|
||||
/* Inline / line synced content highlight */
|
||||
.jira-sync-content {
|
||||
background: color-mix(in srgb, var(--interactive-accent) 12%, transparent);
|
||||
border-radius: 3px;
|
||||
padding: 0 2px;
|
||||
outline: 1px solid color-mix(in srgb, var(--interactive-accent) 35%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Block content highlight — subtle left border between block-start and block-end */
|
||||
/* Block content lines — left accent border */
|
||||
.jira-sync-block-content {
|
||||
border-left: 2px solid var(--interactive-accent) !important;
|
||||
border-left: 3px solid var(--interactive-accent) !important;
|
||||
padding-left: 8px !important;
|
||||
background: color-mix(in srgb, var(--interactive-accent) 5%, transparent) !important;
|
||||
}
|
||||
|
||||
/* JQL Preview Container */
|
||||
|
|
|
|||
Loading…
Reference in a new issue