feat: implement collapsible details blocks with summary widget and styling

This commit is contained in:
Vadim Melnicuk
2026-03-01 17:49:28 +00:00
parent 0ceb8aabec
commit 5a9e15e2c5
4 changed files with 480 additions and 87 deletions
+216 -86
View File
@@ -1,11 +1,26 @@
import { RangeSetBuilder, StateEffect, StateField, Transaction, EditorState } from '@codemirror/state';
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
import { createElement, ChevronDown } from 'lucide';
import { extractHeadingSections, HeadingSection } from './markdownSyntax';
import { extractDetailsBlocks, extractHeadingSections, HeadingSection, DetailsBlockInfo } from './markdownSyntax';
const toggleHeadingCollapseEffect = StateEffect.define<number>();
const expandHeadingCollapseEffect = StateEffect.define<number[]>();
const emptyCollapsedHeadings: readonly number[] = Object.freeze([]);
const emptyCollapseOverrides = Object.freeze(new Map<number, boolean>());
interface CollapsibleSection {
kind: 'heading' | 'details';
anchor: number;
lineFrom: number;
collapseFrom: number;
collapseTo: number;
defaultCollapsed: boolean;
headingSection: HeadingSection | null;
detailsBlock: DetailsBlockInfo | null;
}
export interface DetailsBlockState extends DetailsBlockInfo {
collapsed: boolean;
}
function isHeadingSectionCollapsible(state: EditorState, section: HeadingSection): boolean {
if (!section || section.collapseTo <= section.collapseFrom) {
@@ -14,13 +29,52 @@ function isHeadingSectionCollapsible(state: EditorState, section: HeadingSection
return state.doc.sliceString(section.collapseFrom, section.collapseTo).trim().length > 0;
}
function createHeadingCollapsibleSection(section: HeadingSection): CollapsibleSection {
return {
kind: 'heading',
anchor: section.lineFrom,
lineFrom: section.lineFrom,
collapseFrom: section.collapseFrom,
collapseTo: section.collapseTo,
defaultCollapsed: false,
headingSection: section,
detailsBlock: null
};
}
function createDetailsCollapsibleSection(detailsBlock: DetailsBlockInfo): CollapsibleSection {
return {
kind: 'details',
anchor: detailsBlock.anchorFrom,
lineFrom: detailsBlock.lineFrom,
collapseFrom: detailsBlock.bodyFrom,
collapseTo: detailsBlock.bodyTo,
defaultCollapsed: detailsBlock.defaultCollapsed,
headingSection: null,
detailsBlock
};
}
function getCollapsibleHeadingSections(state: EditorState): HeadingSection[] {
return extractHeadingSections(state).filter((section) => isHeadingSectionCollapsible(state, section));
}
function getCollapsibleHeadingSectionMap(state: EditorState): Map<number, HeadingSection> {
const sections = getCollapsibleHeadingSections(state);
return new Map(sections.map((section) => [section.lineFrom, section]));
function getCollapsibleSections(state: EditorState): CollapsibleSection[] {
const sections = [
...getCollapsibleHeadingSections(state).map(createHeadingCollapsibleSection),
...extractDetailsBlocks(state).map(createDetailsCollapsibleSection)
];
sections.sort((a, b) => a.lineFrom - b.lineFrom || a.anchor - b.anchor);
return sections;
}
function getCollapsibleSectionLineMap(state: EditorState): Map<number, CollapsibleSection> {
return new Map(getCollapsibleSections(state).map((section) => [section.lineFrom, section]));
}
function getCollapsibleSectionAnchorMap(state: EditorState): Map<number, CollapsibleSection> {
return new Map(getCollapsibleSections(state).map((section) => [section.anchor, section]));
}
function hasHeadingCollapseEffect(transaction: any): boolean {
@@ -35,127 +89,216 @@ function hasToggleHeadingCollapseEffect(transaction: any): boolean {
return transaction.effects.some((effect: any) => effect.is(toggleHeadingCollapseEffect));
}
function mapCollapsedHeadingAnchors(anchors: readonly number[], transaction: any): number[] {
if (!anchors.length || !transaction.docChanged) {
return anchors.slice();
function mapCollapseOverrides(
overrides: ReadonlyMap<number, boolean>,
transaction: Transaction
): Map<number, boolean> {
if (!overrides.size || !transaction.docChanged) {
return new Map(overrides);
}
return anchors.map((lineFrom) => transaction.changes.mapPos(lineFrom, 1));
const mapped = new Map<number, boolean>();
for (const [anchor, collapsed] of overrides) {
mapped.set(transaction.changes.mapPos(anchor, 1), collapsed);
}
return mapped;
}
function normalizeCollapsedHeadingAnchors(state: EditorState, anchors: number[]): readonly number[] {
if (!anchors.length) {
return emptyCollapsedHeadings;
function normalizeCollapseOverrides(
state: EditorState,
overrides: Map<number, boolean>
): ReadonlyMap<number, boolean> {
if (!overrides.size) {
return emptyCollapseOverrides;
}
const validLineStarts = new Set(getCollapsibleHeadingSections(state).map((section) => section.lineFrom));
const normalized: number[] = [];
const sections = getCollapsibleSectionAnchorMap(state);
const normalizedEntries: Array<[number, boolean]> = [];
const seen = new Set<number>();
for (const lineFrom of anchors) {
if (!validLineStarts.has(lineFrom) || seen.has(lineFrom)) {
for (const [anchor, collapsed] of overrides) {
if (seen.has(anchor)) {
continue;
}
seen.add(lineFrom);
normalized.push(lineFrom);
seen.add(anchor);
const section = sections.get(anchor);
if (!section || collapsed === section.defaultCollapsed) {
continue;
}
normalizedEntries.push([anchor, collapsed]);
}
normalized.sort((a, b) => a - b);
return normalized.length ? normalized : emptyCollapsedHeadings;
if (!normalizedEntries.length) {
return emptyCollapseOverrides;
}
normalizedEntries.sort((a, b) => a[0] - b[0]);
return new Map(normalizedEntries);
}
function arraysEqual(a: readonly number[], b: readonly number[]): boolean {
function collapseOverridesEqual(
a: ReadonlyMap<number, boolean>,
b: ReadonlyMap<number, boolean>
): boolean {
if (a === b) {
return true;
}
if (a.length !== b.length) {
if (a.size !== b.size) {
return false;
}
for (let index = 0; index < a.length; index += 1) {
if (a[index] !== b[index]) {
const aEntries = a.entries();
const bEntries = b.entries();
while (true) {
const nextA = aEntries.next();
const nextB = bEntries.next();
if (nextA.done || nextB.done) {
return nextA.done === nextB.done;
}
if (nextA.value[0] !== nextB.value[0] || nextA.value[1] !== nextB.value[1]) {
return false;
}
}
return true;
}
function sortedNumbersFromSet(values: Set<number>): readonly number[] {
if (!values.size) {
return emptyCollapsedHeadings;
return [];
}
return Array.from(values).sort((a, b) => a - b);
}
function toggleSetNumber(values: Set<number>, value: number): void {
if (values.has(value)) {
values.delete(value);
return;
}
values.add(value);
function getEffectiveCollapsedState(
overrides: ReadonlyMap<number, boolean>,
section: CollapsibleSection
): boolean {
return overrides.get(section.anchor) ?? section.defaultCollapsed;
}
const headingCollapseStateField = StateField.define<readonly number[]>({
create(): readonly number[] {
return emptyCollapsedHeadings;
function setCollapseOverride(
overrides: Map<number, boolean>,
sections: Map<number, CollapsibleSection>,
anchor: number,
collapsed: boolean
): void {
const section = sections.get(anchor);
if (!section) {
return;
}
if (collapsed === section.defaultCollapsed) {
overrides.delete(anchor);
return;
}
overrides.set(anchor, collapsed);
}
function toggleCollapseOverride(
overrides: Map<number, boolean>,
sections: Map<number, CollapsibleSection>,
anchor: number
): void {
const section = sections.get(anchor);
if (!section) {
return;
}
const nextCollapsed = !getEffectiveCollapsedState(overrides, section);
setCollapseOverride(overrides, sections, anchor, nextCollapsed);
}
const headingCollapseStateField = StateField.define<ReadonlyMap<number, boolean>>({
create(): ReadonlyMap<number, boolean> {
return emptyCollapseOverrides;
},
update(collapsedHeadings: readonly number[], transaction: any): readonly number[] {
update(
collapsedHeadings: ReadonlyMap<number, boolean>,
transaction: Transaction
): ReadonlyMap<number, boolean> {
const hasEffectChange = hasHeadingCollapseEffect(transaction);
if (!transaction.docChanged && !hasEffectChange) {
return collapsedHeadings;
}
let next = mapCollapsedHeadingAnchors(collapsedHeadings, transaction);
let next = mapCollapseOverrides(collapsedHeadings, transaction);
const sections = getCollapsibleSectionAnchorMap(transaction.state);
if (hasEffectChange) {
const nextSet = new Set(next);
for (const effect of transaction.effects) {
if (effect.is(toggleHeadingCollapseEffect)) {
toggleSetNumber(nextSet, effect.value);
toggleCollapseOverride(next, sections, effect.value);
continue;
}
if (effect.is(expandHeadingCollapseEffect)) {
for (const lineFrom of effect.value) {
nextSet.delete(lineFrom);
for (const anchor of effect.value) {
setCollapseOverride(next, sections, anchor, false);
}
}
}
next = Array.from(nextSet);
}
const normalized = normalizeCollapsedHeadingAnchors(transaction.state, next);
return arraysEqual(normalized, collapsedHeadings) ? collapsedHeadings : normalized;
const normalized = normalizeCollapseOverrides(transaction.state, next);
return collapseOverridesEqual(normalized, collapsedHeadings) ? collapsedHeadings : normalized;
}
});
const headingCollapseSharedExtension = Object.freeze([headingCollapseStateField]);
function getCollapsedHeadingAnchors(state: EditorState): readonly number[] {
return state.field(headingCollapseStateField, false) ?? emptyCollapsedHeadings;
function getCollapseOverrides(state: EditorState): ReadonlyMap<number, boolean> {
return state.field(headingCollapseStateField, false) ?? emptyCollapseOverrides;
}
function isHeadingCollapsed(state: EditorState, lineFrom: number): boolean {
return getCollapsedHeadingAnchors(state).includes(lineFrom);
function isSectionCollapsed(state: EditorState, section: CollapsibleSection): boolean {
return getEffectiveCollapsedState(getCollapseOverrides(state), section);
}
function isSectionCollapsedByAnchor(state: EditorState, anchor: number, defaultCollapsed: boolean): boolean {
return getCollapseOverrides(state).get(anchor) ?? defaultCollapsed;
}
function getCollapsedSections(state: EditorState): CollapsibleSection[] {
return getCollapsibleSections(state).filter((section) => isSectionCollapsed(state, section));
}
export function getCollapsedHeadingSections(state: EditorState): HeadingSection[] {
const collapsedLineStarts = getCollapsedHeadingAnchors(state);
if (!collapsedLineStarts.length) {
return [];
return getCollapsedSections(state)
.filter((section) => section.kind === 'heading' && section.headingSection)
.map((section) => section.headingSection as HeadingSection);
}
export function getDetailsBlocks(state: EditorState): DetailsBlockState[] {
return extractDetailsBlocks(state).map((detailsBlock) => ({
...detailsBlock,
collapsed: isSectionCollapsedByAnchor(state, detailsBlock.anchorFrom, detailsBlock.defaultCollapsed)
}));
}
export function toggleCollapsibleSection(view: EditorView, anchor: number): boolean {
const section = getCollapsibleSectionAnchorMap(view.state).get(anchor);
if (!section) {
return false;
}
const sectionMap = getCollapsibleHeadingSectionMap(state);
const sections: HeadingSection[] = [];
for (const lineFrom of collapsedLineStarts) {
const section = sectionMap.get(lineFrom);
if (section) {
sections.push(section);
}
const isCollapsed = isSectionCollapsed(view.state, section);
const transactionSpec: any = {
effects: toggleHeadingCollapseEffect.of(section.anchor),
annotations: Transaction.addToHistory.of(false)
};
if (!isCollapsed && section.kind === 'heading') {
transactionSpec.selection = { anchor: section.lineFrom };
}
return sections.length ? sections : [];
view.dispatch(transactionSpec);
view.focus();
return true;
}
function collectCollapsedHeadingAnchorsForSelection(state: EditorState): readonly number[] {
const collapsedSections = getCollapsedHeadingSections(state);
const collapsedSections = getCollapsedSections(state);
if (!collapsedSections.length) {
return emptyCollapsedHeadings;
return [];
}
const matches = new Set<number>();
@@ -165,12 +308,12 @@ function collectCollapsedHeadingAnchorsForSelection(state: EditorState): readonl
for (const section of collapsedSections) {
if (selectionRange.empty) {
if (from > section.collapseFrom && from < section.collapseTo) {
matches.add(section.lineFrom);
matches.add(section.anchor);
}
continue;
}
if (from < section.collapseTo && to > section.collapseFrom) {
matches.add(section.lineFrom);
matches.add(section.anchor);
}
}
}
@@ -230,12 +373,11 @@ const headingFoldGutterSpacerMarker = new HeadingFoldGutterSpacerMarker();
function buildHeadingFoldGutterMarkers(state: EditorState): any {
const builder = new RangeSetBuilder<any>();
const collapsedAnchors = new Set(getCollapsedHeadingAnchors(state));
for (const section of getCollapsibleHeadingSections(state)) {
for (const section of getCollapsibleSections(state)) {
builder.add(
section.lineFrom,
section.lineFrom,
collapsedAnchors.has(section.lineFrom) ? collapsedHeadingFoldMarker : expandedHeadingFoldMarker
isSectionCollapsed(state, section) ? collapsedHeadingFoldMarker : expandedHeadingFoldMarker
);
}
return builder.finish();
@@ -269,24 +411,12 @@ const liveHeadingFoldGutterExtension = gutter({
return false;
}
const section = getCollapsibleHeadingSectionMap(view.state).get(line.from);
const section = getCollapsibleSectionLineMap(view.state).get(line.from);
event.preventDefault();
event.stopPropagation();
if (!section) {
return true;
if (section) {
toggleCollapsibleSection(view, section.anchor);
}
const isCollapsed = isHeadingCollapsed(view.state, section.lineFrom);
const transactionSpec: any = {
effects: toggleHeadingCollapseEffect.of(section.lineFrom),
annotations: Transaction.addToHistory.of(false)
};
if (!isCollapsed) {
transactionSpec.selection = { anchor: section.lineFrom };
}
view.dispatch(transactionSpec);
view.focus();
return true;
}
}
@@ -297,18 +427,18 @@ const liveHeadingAutoExpandSelectionExtension = EditorView.updateListener.of((up
return;
}
const collapsedAnchors = getCollapsedHeadingAnchors(update.state);
if (!collapsedAnchors.length) {
const collapsedSections = getCollapsedSections(update.state);
if (!collapsedSections.length) {
return;
}
const expandLineStarts = collectCollapsedHeadingAnchorsForSelection(update.state);
if (!expandLineStarts.length) {
const expandAnchors = collectCollapsedHeadingAnchorsForSelection(update.state);
if (!expandAnchors.length) {
return;
}
update.view.dispatch({
effects: expandHeadingCollapseEffect.of(expandLineStarts as number[]),
effects: expandHeadingCollapseEffect.of(expandAnchors as number[]),
annotations: Transaction.addToHistory.of(false)
});
});
+113
View File
@@ -32,6 +32,28 @@ export interface HeadingSection extends HeadingInfo {
collapseTo: number;
}
export interface DetailsBlockInfo {
kind: 'details';
anchorFrom: number;
anchorTo: number;
summaryFrom: number;
summaryTo: number;
lineFrom: number;
lineTo: number;
sectionFrom: number;
sectionTo: number;
bodyFrom: number;
bodyTo: number;
closingFrom: number;
closingTo: number;
summaryText: string;
defaultCollapsed: boolean;
}
const detailsOpenTagPattern = /<details\b[^>]*>/i;
const detailsCloseTagPattern = /<\/details\s*>/i;
const summaryTagPattern = /<summary\b[^>]*>([\s\S]*?)<\/summary\s*>/i;
export function extractHeadings(state: EditorState): HeadingInfo[] {
const headings: HeadingInfo[] = [];
const tree = resolvedSyntaxTree(state);
@@ -56,6 +78,97 @@ export function extractHeadings(state: EditorState): HeadingInfo[] {
return headings;
}
function hasDetailsOpenAttribute(openTag: string): boolean {
const attributeText = openTag
.replace(/^<details\b/i, '')
.replace(/>$/, '');
return /(?:^|[\s/])open(?=[\s=/>]|$)/i.test(attributeText);
}
function normalizeSummaryText(rawText: string | undefined): string {
const text = String(rawText ?? '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return text || 'Details';
}
export function extractDetailsBlocks(state: EditorState): DetailsBlockInfo[] {
const detailsBlocks: DetailsBlockInfo[] = [];
const pendingBlocks: Array<{
anchorFrom: number;
anchorTo: number;
summaryFrom: number;
summaryTo: number;
lineFrom: number;
lineTo: number;
summaryText: string;
defaultCollapsed: boolean;
}> = [];
const tree = resolvedSyntaxTree(state);
tree.iterate({
enter(node: any) {
if (node.name !== 'HTMLBlock') {
return;
}
const rawText = state.doc.sliceString(node.from, node.to);
const openTagMatch = rawText.match(detailsOpenTagPattern);
if (openTagMatch) {
const openingLine = state.doc.lineAt(node.from);
const summaryMatch = rawText.match(summaryTagPattern);
const summaryFrom = typeof summaryMatch?.index === 'number'
? node.from + summaryMatch.index
: node.from;
const summaryTo = typeof summaryMatch?.index === 'number'
? summaryFrom + summaryMatch[0].length
: openingLine.to;
pendingBlocks.push({
anchorFrom: node.from,
anchorTo: node.to,
summaryFrom,
summaryTo,
lineFrom: openingLine.from,
lineTo: openingLine.to,
summaryText: normalizeSummaryText(summaryMatch?.[1]),
defaultCollapsed: !hasDetailsOpenAttribute(openTagMatch[0])
});
}
if (!detailsCloseTagPattern.test(rawText)) {
return;
}
const openBlock = pendingBlocks.pop();
if (!openBlock) {
return;
}
detailsBlocks.push({
kind: 'details',
anchorFrom: openBlock.anchorFrom,
anchorTo: openBlock.anchorTo,
summaryFrom: openBlock.summaryFrom,
summaryTo: openBlock.summaryTo,
lineFrom: openBlock.lineFrom,
lineTo: openBlock.lineTo,
sectionFrom: openBlock.anchorFrom,
sectionTo: node.to,
bodyFrom: openBlock.anchorTo,
bodyTo: node.from,
closingFrom: node.from,
closingTo: node.to,
summaryText: openBlock.summaryText,
defaultCollapsed: openBlock.defaultCollapsed
});
}
});
detailsBlocks.sort((a, b) => a.anchorFrom - b.anchorFrom);
return detailsBlocks;
}
export function extractHeadingSections(state: EditorState): HeadingSection[] {
const headings: HeadingSection[] = [];
const tree = resolvedSyntaxTree(state);
+119 -1
View File
@@ -21,7 +21,9 @@ import { headingLevelFromName, resolvedSyntaxTree } from './helpers/markdownSynt
import {
headingCollapseLiveExtensions,
headingCollapseSharedExtensions,
getCollapsedHeadingSections
getCollapsedHeadingSections,
getDetailsBlocks,
toggleCollapsibleSection
} from './helpers/headingCollapse';
import {
addListMarkerDecoration,
@@ -78,6 +80,7 @@ const lineStyleDecos = {
h4: Decoration.line({ class: 'meo-md-h4' }),
h5: Decoration.line({ class: 'meo-md-h5' }),
h6: Decoration.line({ class: 'meo-md-h6' }),
detailsSummary: Decoration.line({ class: 'meo-md-details-summary-line' }),
quote: Decoration.line({ class: 'meo-md-quote' }),
mergeIncomingHeader: Decoration.line({ class: 'meo-merge-line meo-merge-incoming-header' }),
codeBlock: Decoration.line({ class: 'meo-md-code-block' }),
@@ -401,6 +404,61 @@ function frontmatterArrayPillsWidget(itemLabels) {
return widget;
}
class DetailsSummaryWidget extends WidgetType {
anchor: number;
lineFrom: number;
summaryText: string;
collapsed: boolean;
constructor(anchor: number, lineFrom: number, summaryText: string, collapsed: boolean) {
super();
this.anchor = anchor;
this.lineFrom = lineFrom;
this.summaryText = summaryText;
this.collapsed = collapsed;
}
eq(other: WidgetType): boolean {
return (
other instanceof DetailsSummaryWidget &&
other.anchor === this.anchor &&
other.lineFrom === this.lineFrom &&
other.summaryText === this.summaryText &&
other.collapsed === this.collapsed
);
}
toDOM(view: EditorView): HTMLElement {
const button = document.createElement('button');
button.type = 'button';
button.className = 'meo-md-details-summary';
button.title = this.collapsed ? 'Expand details' : 'Collapse details';
button.setAttribute('aria-label', this.collapsed ? 'Expand details' : 'Collapse details');
const label = document.createElement('span');
label.className = 'meo-md-details-summary-label';
label.textContent = this.summaryText;
button.appendChild(label);
button.addEventListener('pointerdown', (event) => {
event.preventDefault();
event.stopPropagation();
});
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
toggleCollapsibleSection(view, this.anchor);
});
return button;
}
ignoreEvent(): boolean {
return true;
}
}
function addMarkdownLinkDecorations(builder, state, node, activeLines) {
const urlNode = findChildNode(node, 'URL');
if (!urlNode) {
@@ -584,6 +642,64 @@ function addLineClass(builder, state, from, to, deco) {
}
}
function rangeTouchesActiveLine(state: EditorState, from: number, to: number, activeLines: Set<number>): boolean {
if (to <= from) {
return false;
}
const startLine = state.doc.lineAt(from).number;
const endLine = state.doc.lineAt(Math.max(from, to - 1)).number;
for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
if (activeLines.has(lineNo)) {
return true;
}
}
return false;
}
function addDetailsBlockDecorations(builder, state, detailsBlocks, activeLines) {
for (const detailsBlock of detailsBlocks) {
const openingActive = rangeTouchesActiveLine(state, detailsBlock.anchorFrom, detailsBlock.anchorTo, activeLines);
const closingActive = rangeTouchesActiveLine(state, detailsBlock.closingFrom, detailsBlock.closingTo, activeLines);
const editingBoundary = openingActive || closingActive;
if (!editingBoundary) {
addLineClass(builder, state, detailsBlock.lineFrom, detailsBlock.lineTo, lineStyleDecos.detailsSummary);
if (detailsBlock.summaryFrom > detailsBlock.anchorFrom) {
builder.push(
collapsedHeadingBodyDeco.range(detailsBlock.anchorFrom, detailsBlock.summaryFrom)
);
}
builder.push(
Decoration.replace({
widget: new DetailsSummaryWidget(
detailsBlock.anchorFrom,
detailsBlock.lineFrom,
detailsBlock.summaryText,
detailsBlock.collapsed
)
}).range(detailsBlock.summaryFrom, detailsBlock.summaryTo)
);
if (detailsBlock.anchorTo > detailsBlock.summaryTo) {
builder.push(
collapsedHeadingBodyDeco.range(detailsBlock.summaryTo, detailsBlock.anchorTo)
);
}
}
if (!editingBoundary) {
builder.push(collapsedHeadingBodyDeco.range(detailsBlock.closingFrom, detailsBlock.closingTo));
}
if (detailsBlock.collapsed && detailsBlock.bodyTo > detailsBlock.bodyFrom) {
builder.push(collapsedHeadingBodyDeco.range(detailsBlock.bodyFrom, detailsBlock.bodyTo));
}
}
}
function addAtxHeadingPrefixMarkers(builder, state, from, activeLines) {
const line = state.doc.lineAt(from);
const text = state.doc.sliceString(line.from, line.to);
@@ -664,6 +780,7 @@ function buildDecorations(state) {
const indentSelectedLines = collectIndentSelectedLines(state);
const tree = resolvedSyntaxTree(state);
const collapsedHeadingSections = getCollapsedHeadingSections(state);
const detailsBlocks = getDetailsBlocks(state);
const strikeRanges = collectStrikethroughRanges(tree);
const codeBlockLines = collectCodeBlockLines(state, tree);
const parsedTableRanges = [];
@@ -868,6 +985,7 @@ function buildDecorations(state) {
addSingleTildeStrikeDecorations(ranges, state, activeLines, strikeRanges, codeBlockLines);
addListLineDecorations(ranges, state, indentSelectedLines, frontmatter, codeBlockLines);
addEmojiDecorations(ranges, state, codeBlockLines);
addDetailsBlockDecorations(ranges, state, detailsBlocks, activeLines);
for (const section of collapsedHeadingSections) {
addLineClass(ranges, state, section.lineFrom, section.lineTo, collapsedHeadingLineDeco);
addRange(ranges, section.collapseFrom, section.collapseTo, collapsedHeadingBodyDeco);
+32
View File
@@ -666,6 +666,38 @@ body {
transform: rotate(-90deg);
}
.cm-editor .meo-md-details-summary {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 1px 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
border-radius: 3px;
}
.cm-editor .cm-line.meo-md-details-summary-line {
box-shadow: inset 0 -1px 0 color-mix(
in srgb,
var(--meo-color-base03, var(--vscode-editorLineNumber-foreground)) 70%,
transparent
);
}
.cm-editor .meo-md-details-summary:hover,
.cm-editor .meo-md-details-summary:focus-visible {
color: var(--vscode-editor-foreground);
outline: none;
}
.cm-editor .meo-md-details-summary-label {
display: inline;
}
.cm-editor .cm-gutterElement.cm-activeLineGutter {
background: transparent !important;
color: var(--vscode-editor-foreground);