Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186// src/editor/__tests__/markdown-context.test.ts//// Pin-down tests for the public contract of `markdown-context.ts` helpers,// specifically the boundary behavior of `isRangeInCodeContext` and// `isRangeInBlockCodeContext`://// 1. End-of-document offsets must NOT be silently shifted to `length - 1`// in a way that flips the answer for queries right at end-of-doc.// 2. Empty ranges (`from === to`) are treated as offset queries at `from`,// so a blank line inside a fenced code block still reports as code.//// These tests pin the conservative answers required by callers (e.g.// callout-decoration), which expect blank lines and end-of-doc positions// inside fenced code to be excluded from markdown enhancement.import { describe, it, expect } from 'vitest'import { EditorState } from '@codemirror/state'import { markdown, markdownLanguage } from '@codemirror/lang-markdown'import { GFM } from '@lezer/markdown'import { isRangeInBlockCodeContext, isRangeInCodeContext, isOffsetRangeInTableCell, getTablePositionAt, markdownFrontmatterField,} from '../extensions/markdown-context'import { reamsMarkdownLanguage } from '../markdown-language'
function createState(doc: string): EditorState { return EditorState.create({ doc, extensions: [ markdown({ base: markdownLanguage, extensions: [GFM], addKeymap: false }), markdownFrontmatterField, ], })}
describe('markdown-context — end-of-document boundary', () => { it('reports an end-of-doc empty-range query inside a fenced code block as code', () => { // Fence with no trailing newline — the end-of-doc offset is inside the // fence. Previously the clamp to `length - 1` could flip the answer here. const doc = '```\ncode\n```' const state = createState(doc) const eod = state.doc.length
// Empty-range query exactly at end-of-doc. expect(isRangeInBlockCodeContext(state, eod, eod)).toBe(true) expect(isRangeInCodeContext(state, eod, eod)).toBe(true) })
it('reports a non-empty range that ends at end-of-doc inside a fence as code', () => { const doc = '```\ncode\n```' const state = createState(doc) const eod = state.doc.length
// Range covering the closing fence backticks up to end-of-doc. const from = doc.lastIndexOf('```') expect(isRangeInBlockCodeContext(state, from, eod)).toBe(true) })})
describe('markdown-context — empty range contract', () => { it('treats an empty range on a blank line inside a fenced code block as code', () => { // Blank line between two code lines must still report as inside the fence // even though `iterate` over an empty range is a no-op. const doc = '```\nfirst\n\nlast\n```\n' const state = createState(doc) // Offset of the blank line (the second \n is the blank line position). const blankLineStart = doc.indexOf('first\n') + 'first\n'.length expect(state.doc.lineAt(blankLineStart).text).toBe('')
expect(isRangeInBlockCodeContext(state, blankLineStart, blankLineStart)).toBe(true) expect(isRangeInCodeContext(state, blankLineStart, blankLineStart)).toBe(true) })
it('reports an empty range outside any code context as not in code', () => { const doc = 'plain prose paragraph\n' const state = createState(doc) const offset = 5
expect(isRangeInBlockCodeContext(state, offset, offset)).toBe(false) expect(isRangeInCodeContext(state, offset, offset)).toBe(false) })})
describe('markdown-context — delimiter-free inline-code tree', () => { it('treats opening delimiter, content, and closing delimiter as code context', () => { const doc = 'before `code` after' const state = EditorState.create({ doc, extensions: [ markdown({ base: reamsMarkdownLanguage, extensions: [GFM], addKeymap: false }), markdownFrontmatterField, ], })
for (const offset of [7, 8, 11, 12]) { expect(isRangeInCodeContext(state, offset, offset)).toBe(true) } expect(isRangeInCodeContext(state, 13, 13)).toBe(false) expect(isRangeInCodeContext(state, 7, 9)).toBe(true) expect(isRangeInCodeContext(state, 11, 13)).toBe(true) })
it('recognizes every position in a variable-length code span', () => { const doc = 'before ``a ` b`` after' const state = EditorState.create({ doc, extensions: [markdown({ base: reamsMarkdownLanguage, extensions: [GFM], addKeymap: false })], }) const start = doc.indexOf('``') const end = doc.lastIndexOf('``') + 2
for (let offset = start; offset < end; offset++) { expect(isRangeInCodeContext(state, offset, offset)).toBe(true) } expect(isRangeInCodeContext(state, end, end)).toBe(false) })})
describe('markdown-context — empty document and range edges', () => { it('reports an empty document as not in any context', () => { const state = createState('') expect(isRangeInCodeContext(state, 0, 0)).toBe(false) expect(isRangeInBlockCodeContext(state, 0, 0)).toBe(false) expect(isOffsetRangeInTableCell(state, 0, 0)).toBe(false) expect(getTablePositionAt(state, 0)).toBeNull() })
it('treats a range starting outside a fence but ending inside it as code', () => { // Fence starts at offset 2; from=0 is outside, to-1=4 is inside. const doc = 'x\n```\ncode\n```\n' const state = createState(doc) expect(isRangeInBlockCodeContext(state, 0, 5)).toBe(true) })
it('handles end-of-document offsets in table helpers', () => { const doc = '| A | B |\n|---|---|\n| 1 | 2 |\n' const state = createState(doc) const eod = state.doc.length expect(isOffsetRangeInTableCell(state, eod, eod)).toBe(false) expect(getTablePositionAt(state, eod)).toBeNull() })
it('rejects a range straddling a table cell boundary', () => { // Cell 1 spans [2, 3) in the header row; a range overlapping the // boundary must not be treated as fully inside the cell. const doc = '| A | B |\n|---|---|\n| 1 | 2 |\n' const state = createState(doc) expect(isOffsetRangeInTableCell(state, 2, 3)).toBe(true) expect(isOffsetRangeInTableCell(state, 2, 6)).toBe(false) expect(isOffsetRangeInTableCell(state, 1, 6)).toBe(false) })})
describe('markdown-context — table separator positions', () => { // '| A | B |\n' (0-9) // '|---|---|\n' (10-19) separator; pipes at 10, 14, 18 // '| 1 | 2 |\n' (20-29) const TABLE = '| A | B |\n|---|---|\n| 1 | 2 |\n'
it('resolves cursor positions on the separator line', () => { const state = createState(TABLE) expect(getTablePositionAt(state, 12)).toMatchObject({ rowKind: 'separator', columnIndex: 0, cellOffset: 1 }) expect(getTablePositionAt(state, 16)).toMatchObject({ rowKind: 'separator', columnIndex: 1, cellOffset: 1 }) })
it('snaps a cursor exactly on a separator pipe to the next cell', () => { const state = createState(TABLE) expect(getTablePositionAt(state, 10)).toMatchObject({ rowKind: 'separator', columnIndex: 0, cellOffset: 0 }) expect(getTablePositionAt(state, 14)).toMatchObject({ rowKind: 'separator', columnIndex: 1, cellOffset: 0 }) })
it('resolves the trailing separator pipe to the last cell gap', () => { const state = createState(TABLE) expect(getTablePositionAt(state, 18)).toMatchObject({ rowKind: 'separator', columnIndex: 1, cellOffset: 3 }) })
it('leaves a separator cursor at the line end unresolved', () => { const state = createState(TABLE) // Position 19 is the separator line's newline (sep.to), outside every // [pipe, pipe] segment. expect(getTablePositionAt(state, 19)).toMatchObject({ rowKind: 'separator', columnIndex: -1, cellOffset: -1 }) })})