Current behavior
Mark switching is asymmetric today, but not in the way you'd expect — both directions between bold and link are blocked, by two different mechanisms:
- link → bold is blocked by
can_switch_mark_type (src/lib/doc_utils.ts), which only allows switching when both mark types are property-less. Link carries href, so converting away from a link is refused. This is deliberate data-loss protection and should stay.
- bold → link is blocked by a blunter rule:
ToggleLinkCommand.is_enabled (src/routes/commands.svelte.ts) requires !selection_touches_marks for link creation. Any touched mark — even a property-less bold — disables the link button. The "replace a property-less mark with a link" path was simply never implemented; there is no data-loss argument for it.
Proposal
Allow switching a single touched property-less mark (strong, emphasis, code, …) to a link. Nothing is lost: marks are mutually exclusive, the source mark carries no data, and the link's href is supplied by the URL prompt. The reverse direction stays forbidden via can_switch_mark_type.
Proposed change to ToggleLinkCommand (for discussion, not yet implemented):
is_enabled() {
const { session, editable } = this.context;
const selected_marks = session.selected_marks;
const selection_touches_marks = selected_marks.length > 0;
const can_remove_link = selected_marks.length === 1 && selected_marks[0].node.type === 'link';
// A single property-less mark (e.g. strong, emphasis) may be switched to
// a link: nothing is lost, the href is supplied by the prompt.
const can_switch_to_link =
selected_marks.length === 1 &&
!can_remove_link &&
Object.keys(session.schema[selected_marks[0].node.type]?.properties ?? {}).length === 0;
const can_create_link =
!selection_touches_marks &&
session.selection?.type === 'text' &&
!is_selection_collapsed(session.selection);
return Boolean(
editable &&
session.selection?.type === 'text' &&
(can_remove_link || can_create_link || can_switch_to_link)
);
}
execute() {
const session = this.context.session;
const selected_marks = session.selected_marks;
const can_remove_link = selected_marks.length === 1 && selected_marks[0].node.type === 'link';
if (can_remove_link) {
// Delete link
session.apply(session.tr.toggle_mark('link'));
} else {
// Create link, or switch a single property-less mark to a link
const href = window.prompt('Enter the URL', 'https://example.com');
if (href) {
session.apply(session.tr.toggle_mark('link', { href }));
}
}
}
Open questions
- Does
tr.toggle_mark('link', { href }) correctly displace the touched property-less mark in the switch case? The generic switch path in ToggleMarkCommand calls it without properties — needs verification plus a test.
- Behavior for partially touched marks: should switching require the selection to cover the mark, or is touch enough (current
ToggleMarkCommand switch semantics)?
- Should this generalize? Any future data-carrying mark command (e.g. a comment mark) would want the same pattern: switching to it is fine when the source is property-less and the target's properties are collected in its own UI. Could be a shared helper, e.g.
can_switch_to_mark_type(schema, from_type) as the source-side half of can_switch_mark_type.
Current behavior
Mark switching is asymmetric today, but not in the way you'd expect — both directions between bold and link are blocked, by two different mechanisms:
can_switch_mark_type(src/lib/doc_utils.ts), which only allows switching when both mark types are property-less. Link carrieshref, so converting away from a link is refused. This is deliberate data-loss protection and should stay.ToggleLinkCommand.is_enabled(src/routes/commands.svelte.ts) requires!selection_touches_marksfor link creation. Any touched mark — even a property-less bold — disables the link button. The "replace a property-less mark with a link" path was simply never implemented; there is no data-loss argument for it.Proposal
Allow switching a single touched property-less mark (strong, emphasis, code, …) to a link. Nothing is lost: marks are mutually exclusive, the source mark carries no data, and the link's
hrefis supplied by the URL prompt. The reverse direction stays forbidden viacan_switch_mark_type.Proposed change to
ToggleLinkCommand(for discussion, not yet implemented):Open questions
tr.toggle_mark('link', { href })correctly displace the touched property-less mark in the switch case? The generic switch path inToggleMarkCommandcalls it without properties — needs verification plus a test.ToggleMarkCommandswitch semantics)?can_switch_to_mark_type(schema, from_type)as the source-side half ofcan_switch_mark_type.