diff --git a/src/postprocessing/livePreview.ts b/src/postprocessing/livePreview.ts index 909ee72..6bfff41 100644 --- a/src/postprocessing/livePreview.ts +++ b/src/postprocessing/livePreview.ts @@ -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(); 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); } }, { diff --git a/src/postprocessing/reading.ts b/src/postprocessing/reading.ts index 8b614a3..293e32b 100644 --- a/src/postprocessing/reading.ts +++ b/src/postprocessing/reading.ts @@ -1,20 +1,73 @@ import JiraPlugin from "../main"; -// For Reading mode - targets elements in

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; +} diff --git a/styles.css b/styles.css index 90b6846..5613524 100644 --- a/styles.css +++ b/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 */