|
|
@@ -9,17 +9,9 @@
|
|
|
* - Tags #tag
|
|
|
* - Property lines (key:: value)
|
|
|
*
|
|
|
- * Focus-based rendering:
|
|
|
- * - When the block has focus: no decorations (raw text for editing)
|
|
|
- * - When the block is blurred: full decorations (styled preview)
|
|
|
- *
|
|
|
- * Architecture:
|
|
|
- * - Rules provide wrapperClass and wrapperAttrs per token
|
|
|
- * - ProseMirror decorations are flat, so wrapper attrs are stamped
|
|
|
- * onto every segment — no structural dependency between spans
|
|
|
- * - Token array is the source of truth — decorations are a lazy projection
|
|
|
- * built in props.decorations. Tokens are only re-parsed on document
|
|
|
- * changes, not cursor moves.
|
|
|
+ * Marker visibility modes:
|
|
|
+ * - 'live-preview': markers hidden when blurred, revealed near cursor when focused
|
|
|
+ * - 'always-visible': markers always visible (traditional markdown editor feel)
|
|
|
*/
|
|
|
|
|
|
import type { Node as ProsemirrorNode } from "prosemirror-model"
|
|
|
@@ -31,7 +23,9 @@ import {
|
|
|
parseMarkdown,
|
|
|
} from "../lib/markdown-parser"
|
|
|
|
|
|
-type DecorationSpec = ReturnType<typeof Decoration.inline>
|
|
|
+export type MarkerVisibilityMode = "live-preview" | "always-visible"
|
|
|
+
|
|
|
+const defaultMarkerMode: MarkerVisibilityMode = "live-preview"
|
|
|
|
|
|
interface ParsedToken {
|
|
|
type: string
|
|
|
@@ -105,11 +99,15 @@ function parseBlockTokens(
|
|
|
/**
|
|
|
* Build decorations from cached tokens.
|
|
|
* Runs lazily in props.decorations — only when ProseMirror asks for them.
|
|
|
- * No cursor tracking, no pattern suppression — all ranges always visible.
|
|
|
+ * Handles live preview by toggling marker visibility based on cursor proximity.
|
|
|
*/
|
|
|
function buildDecorationsFromCache(
|
|
|
doc: ProsemirrorNode,
|
|
|
contentCaches: Map<string, BlockTokenCache>,
|
|
|
+ isFocused: boolean,
|
|
|
+ _cursorFrom: number,
|
|
|
+ _cursorTo: number,
|
|
|
+ markerMode: MarkerVisibilityMode,
|
|
|
): DecorationSet {
|
|
|
const decorationSpecs: DecorationSpec[] = []
|
|
|
|
|
|
@@ -123,14 +121,38 @@ function buildDecorationsFromCache(
|
|
|
if (!cache) return
|
|
|
|
|
|
for (const block of cache.blocks) {
|
|
|
- buildTokenDecorations(block, blockStart, decorationSpecs)
|
|
|
+ buildTokenDecorations(
|
|
|
+ block,
|
|
|
+ blockStart,
|
|
|
+ decorationSpecs,
|
|
|
+ isFocused,
|
|
|
+ _cursorFrom,
|
|
|
+ _cursorTo,
|
|
|
+ markerMode,
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
for (const token of cache.content) {
|
|
|
- buildTokenDecorations(token, blockStart, decorationSpecs)
|
|
|
+ buildTokenDecorations(
|
|
|
+ token,
|
|
|
+ blockStart,
|
|
|
+ decorationSpecs,
|
|
|
+ isFocused,
|
|
|
+ _cursorFrom,
|
|
|
+ _cursorTo,
|
|
|
+ markerMode,
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- buildPropertyDecorations(cache.properties, blockStart, decorationSpecs)
|
|
|
+ buildPropertyDecorations(
|
|
|
+ cache.properties,
|
|
|
+ blockStart,
|
|
|
+ decorationSpecs,
|
|
|
+ isFocused,
|
|
|
+ _cursorFrom,
|
|
|
+ _cursorTo,
|
|
|
+ markerMode,
|
|
|
+ )
|
|
|
})
|
|
|
|
|
|
return DecorationSet.create(doc, decorationSpecs)
|
|
|
@@ -142,11 +164,19 @@ function buildDecorationsFromCache(
|
|
|
* ProseMirror decorations are flat — overlapping ranges produce sibling
|
|
|
* spans, not nested elements. Wrapper attrs are stamped onto every
|
|
|
* segment so each span is self-contained.
|
|
|
+ *
|
|
|
+ * In live preview mode:
|
|
|
+ * - type: "content" ranges always show
|
|
|
+ * - type: "hidden" ranges (markers) fade based on cursor proximity
|
|
|
*/
|
|
|
function buildTokenDecorations(
|
|
|
token: ParsedToken,
|
|
|
offset: number,
|
|
|
decorationSpecs: DecorationSpec[],
|
|
|
+ isFocused: boolean,
|
|
|
+ _cursorFrom: number,
|
|
|
+ _cursorTo: number,
|
|
|
+ markerMode: MarkerVisibilityMode,
|
|
|
) {
|
|
|
const ranges = token.decorationRanges
|
|
|
if (ranges.length === 0) return
|
|
|
@@ -167,9 +197,24 @@ function buildTokenDecorations(
|
|
|
const rangeFrom = offset + range.from
|
|
|
const rangeTo = offset + range.to
|
|
|
|
|
|
- const rangeClass =
|
|
|
+ const isHiddenDecorator = range.type === "hidden"
|
|
|
+
|
|
|
+ let rangeClass =
|
|
|
range.className ??
|
|
|
- (range.type === "hidden" ? "md-decorator" : `md-${range.type}`)
|
|
|
+ (isHiddenDecorator ? "md-decorator" : `md-${range.type}`)
|
|
|
+
|
|
|
+ // Apply visibility rules based on marker mode
|
|
|
+ if (isHiddenDecorator) {
|
|
|
+ if (markerMode === "always-visible") {
|
|
|
+ // Always show markers
|
|
|
+ rangeClass = "md-decorator"
|
|
|
+ } else if (markerMode === "live-preview") {
|
|
|
+ // Live preview: reveal markers when focused
|
|
|
+ rangeClass = isFocused
|
|
|
+ ? "md-decorator"
|
|
|
+ : "md-decorator md-decorator-hidden"
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
const classes = rangeClass
|
|
|
|
|
|
@@ -196,22 +241,33 @@ function buildPropertyDecorations(
|
|
|
}>,
|
|
|
childOffset: number,
|
|
|
decorationSpecs: DecorationSpec[],
|
|
|
+ _isFocused: boolean,
|
|
|
+ _cursorFrom: number,
|
|
|
+ _cursorTo: number,
|
|
|
+ _markerMode: MarkerVisibilityMode,
|
|
|
) {
|
|
|
for (const prop of properties) {
|
|
|
const [keyFrom, keyTo] = prop.keyRange
|
|
|
const [delimFrom, delimTo] = prop.delimiterRange
|
|
|
const [valueFrom, valueTo] = prop.valueRange
|
|
|
|
|
|
+ const keyStart = keyFrom + childOffset
|
|
|
+ const keyEnd = keyTo + childOffset
|
|
|
+ const delimStart = delimFrom + childOffset
|
|
|
+ const delimEnd = delimTo + childOffset
|
|
|
+
|
|
|
+ // Property keys and delimiters always show (they're structural elements)
|
|
|
decorationSpecs.push(
|
|
|
- Decoration.inline(keyFrom + childOffset, keyTo + childOffset, {
|
|
|
+ Decoration.inline(keyStart, keyEnd, {
|
|
|
class: "md-property-key",
|
|
|
}),
|
|
|
)
|
|
|
decorationSpecs.push(
|
|
|
- Decoration.inline(delimFrom + childOffset, delimTo + childOffset, {
|
|
|
+ Decoration.inline(delimStart, delimEnd, {
|
|
|
class: "md-property-delimiter",
|
|
|
}),
|
|
|
)
|
|
|
+
|
|
|
if (prop.value) {
|
|
|
decorationSpecs.push(
|
|
|
Decoration.inline(valueFrom + childOffset, valueTo + childOffset, {
|
|
|
@@ -251,8 +307,11 @@ function invalidateChangedCaches(
|
|
|
return newCaches
|
|
|
}
|
|
|
|
|
|
-export function createMarkdownDecorationsPlugin() {
|
|
|
+export function createMarkdownDecorationsPlugin(
|
|
|
+ markerMode: MarkerVisibilityMode = defaultMarkerMode,
|
|
|
+) {
|
|
|
let hasFocus = false
|
|
|
+ let currentMarkerMode = markerMode
|
|
|
|
|
|
const plugin = new Plugin<DecorationState>({
|
|
|
key: markdownDecorationsKey,
|
|
|
@@ -271,28 +330,39 @@ export function createMarkdownDecorationsPlugin() {
|
|
|
},
|
|
|
|
|
|
apply(tr, prevState, _oldState, newState) {
|
|
|
- if (!tr.docChanged) {
|
|
|
- return prevState
|
|
|
+ // Check for mode change in transaction meta
|
|
|
+ if (tr.getMeta("markerMode") !== undefined) {
|
|
|
+ currentMarkerMode = tr.getMeta("markerMode")
|
|
|
}
|
|
|
|
|
|
- const contentCaches = invalidateChangedCaches(
|
|
|
- newState.doc,
|
|
|
- prevState.contentCaches,
|
|
|
- prevState.engine,
|
|
|
- )
|
|
|
+ if (tr.docChanged) {
|
|
|
+ const contentCaches = invalidateChangedCaches(
|
|
|
+ newState.doc,
|
|
|
+ prevState.contentCaches,
|
|
|
+ prevState.engine,
|
|
|
+ )
|
|
|
+ return { engine: prevState.engine, contentCaches }
|
|
|
+ }
|
|
|
|
|
|
- return { engine: prevState.engine, contentCaches }
|
|
|
+ return prevState
|
|
|
},
|
|
|
},
|
|
|
|
|
|
props: {
|
|
|
decorations(state) {
|
|
|
- if (hasFocus) return DecorationSet.empty
|
|
|
-
|
|
|
const pluginState = markdownDecorationsKey.getState(state)
|
|
|
if (!pluginState) return DecorationSet.empty
|
|
|
|
|
|
- return buildDecorationsFromCache(state.doc, pluginState.contentCaches)
|
|
|
+ const { from, to } = state.selection
|
|
|
+
|
|
|
+ return buildDecorationsFromCache(
|
|
|
+ state.doc,
|
|
|
+ pluginState.contentCaches,
|
|
|
+ hasFocus,
|
|
|
+ from,
|
|
|
+ to,
|
|
|
+ currentMarkerMode,
|
|
|
+ )
|
|
|
},
|
|
|
},
|
|
|
})
|
|
|
@@ -302,6 +372,9 @@ export function createMarkdownDecorationsPlugin() {
|
|
|
setFocus: (focused: boolean) => {
|
|
|
hasFocus = focused
|
|
|
},
|
|
|
+ setMarkerMode: (mode: MarkerVisibilityMode) => {
|
|
|
+ currentMarkerMode = mode
|
|
|
+ },
|
|
|
}
|
|
|
}
|
|
|
|