Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0
- Present active mixed-format link labels as one coordinated source range.
- Extend link label source projection to labels that contain an image, such as badge links.
- Keep a URL written on its own as it was written, bare or between angle brackets, instead of putting angle brackets around every bare URL in the file on the first save.
- Turn a typed link, URL, or angle-bracket URL into the link it describes once the caret leaves it, as pasting the same text already did.

### Fixed

- Read a backslash typed into an open link or footnote-reference source as the escape it spells, so the run turns into the text it describes and saves with one backslash, instead of keeping the backslash as a character and saving three.
- Keep a link or footnote reference whole when a character is typed at the start of its open Markdown source, instead of turning the whole construct into literal text that saves with escapes.
- Write a backslash on save only where the character it precedes would otherwise be read as Markdown, so text such as `garden_sensor_name` keeps its underscores bare, instead of escaping every character that could be syntax somewhere else.
- Keep a list item that starts with a code block, table, quote, nested list, heading, or thematic break nested in the saved file, instead of writing an empty item and leaving the block outside the list the next time the document is opened.
- Keep typed link and autolink source literal in the saved file when a space follows it, instead of writing it as live Markdown that turns into a link the next time the document is opened.
- Escape text the editor keeps literal even when a space follows it, instead of writing it as live Markdown that turns into something else the next time the document is opened.
- Open the Markdown source of a link whose label holds a footnote reference, instead of leaving it closed everywhere in the label except on the reference itself.
- Keep a link label that mixes formatted text with a footnote reference as one link, instead of saving it as two links.
- Open bold, italic, or strikethrough that wraps a link as one Markdown source with the link inside it, instead of one side of the link at a time with markers that do not match the file.
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Source projection temporarily exposes a supported Markdown object as unmarked, e

The shared projection engine owns the active session, projected range, projection-local history, dirty-state integration, and finalization. A clean session restores its original content; an edited session rehydrates valid source or commits literal text so projected characters are not discarded. A projected range holds flat text; a session whose range stops holding it ends without committing, leaving what landed there in the document.

Document text that already spells a supported object, rather than having been projected from one, commits through the same adapter validation when the caret leaves it. The engine commits only inside the ranges the session has written, and never inside a run of source the file escaped, which it recognizes from the state a write lands in before that write changes it. A change that only relocates content the document already held, such as a table row move, declares itself and records nothing as written, because its steps re-insert what they took. The same characters reach the document either way, because the escape does not survive parsing. History clears both records, so an undone commit stays undone.

A change that reaches the projected range without passing through the engine's edit path is an unauthored write; composition input is the path that produces one. The engine keeps an unauthored write out of native history, where it would replay against coordinates the commit discards, and otherwise treats it as the content change it is: the document becomes dirty and projection-local history can step back over it.

Object adapters own target discovery, source generation, validation, rehydration, presentation spans, and selection mapping. Ownership precedence is logical link, qualifying marked fragment, then standalone footnote reference. Adapters that cannot preserve a semantic mapping fall back to literal text.
Expand Down
1 change: 1 addition & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ The editor is a unified hybrid Markdown surface. Behavior is governed by renderi
- Seamless source projection for strong, emphasis, strikethrough, inline code, links, autolinks, and footnote references is local to the active inline object. For mark-based content, a caret or text selection activates projection when it is contained within one exact, contiguous combination of supported inline marks. Editing a projected marker can change that object's inline style, but it does not automatically merge adjacent marked runs; broader reshaping is done with an explicit selection or formatting command.
- Inline-code projection uses a valid canonical backtick delimiter run rather than preserving the exact source delimiter length.
- Link and autolink projection exposes their source directly in the document; links preserve their label, target, optional title, and compatible uniform outer inline formatting. An autolink keeps the form it was authored in, bare or angle-bracket, when it is projected and when it is saved. A link remains one semantic projection owner. A caret or contained text selection anywhere in a supported label projects the complete link source, including labels with nested strong, emphasis, strikethrough, inline-code formatting, semantic soft line endings, an image, or a footnote reference. An image or footnote reference in a projected label becomes its own Markdown source and returns as its object when the label commits. Soft line endings remain one logical label; indentation follows Leafdown's canonical serialization. Valid edits rehydrate one link over the complete rich label; invalid or incomplete edits become the literal text the source spells, where a backslash escapes the character it precedes and is otherwise kept as text. Mixed-format and multiline labels do not fall back to fragmented projections for their nested content.
- Text written in the current editing session that is exactly the source of one inline link, autolink literal, or URI autolink becomes that link when the caret leaves it, including when a line break ends the line it sits on, so typing and plain-text paste of the same characters reach the same document. The caret has not left while only source characters separate them, which keeps a bare URL whole as it is typed. Backslash-escaped source, incomplete source, and text that is not exactly one link's source stay literal. Source the file escaped is literal text the author asked for, so editing it leaves it literal and only replacing it outright commits it, and `Undo` returns a committed link to the source it was written as. Because a committed link projects its source again whenever the caret returns, the visible text does not change.
- A link wrapped by one exact, contiguous supported mark combination belongs to that marked fragment. Entering from either side of the link projects one outer wrapper holding the link's complete source, such as `**bold [a b](./doc.md) tail**`, and a valid edit commits one mark around the link, its label, and its destination. A mark that stops at the link keeps its own projection, and logical links retain higher semantic ownership, so a caret inside the label still projects the link alone.
- A footnote reference within one exact, contiguous supported mark combination belongs to that marked fragment. Entering through its text, either reference boundary, or the atomic reference projects one outer wrapper such as `**archive note[^archive]**`; the complete compatible mark set applies to both text and reference nodes. Logical links retain higher semantic ownership, while standalone or otherwise ineligible references use the reference-only adapter.
- Standalone footnote references project their complete `[^label]` source as editable document text. A caret entering from the left starts at the beginning of the source, a caret entering from the right starts at the end, and selecting an atomic reference selects its label after projection. Valid edits in either projection rehydrate canonical Milkdown footnote-reference nodes, and an invalid edit to a standalone reference becomes the literal text its source spells, on the same escape rule as a link. If a marked wrapper remains valid, incomplete reference-like content remains exact text inside its outer marks; if the outer wrapper becomes invalid, the complete projected source becomes exact unmarked literal text. Editing a reference label does not create, rename, delete, or modify any footnote definition.
Expand Down
5 changes: 4 additions & 1 deletion src/features/editor/commands/formatting/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { EditorView } from "@milkdown/kit/prose/view";

import { areNonNullish } from "@/lib/predicates";

import { SOURCE_PROJECTION_RESTRUCTURE_META } from "../../plugins/sourceProjection";
import { getNodeType, runProseMirrorCommand, setSelectionNear } from "../../utils/milkdown";
import {
getSelectedTableRect,
Expand Down Expand Up @@ -58,7 +59,9 @@ const dispatchTableReplacement = (
) => {
const tablePos = getTablePosition(rect);
const tableStart = tablePos + 1;
const tr = view.state.tr.replaceWith(tablePos, tablePos + rect.table.nodeSize, table);
const tr = view.state.tr
.replaceWith(tablePos, tablePos + rect.table.nodeSize, table)
.setMeta(SOURCE_PROJECTION_RESTRUCTURE_META, true);

if (selectionCell) {
setTableCellSelection(tr, tableStart, table, selectionCell);
Expand Down
Loading