dorfteich/apps/web/src/editor/nodes/text-basics.ts
Claude Fable 5 784f21d805
All checks were successful
CI / Auth e2e pack (push) Successful in 4m10s
CI / Import/export fidelity gate (push) Successful in 45s
CD / Build and push images (push) Successful in 1m0s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 2m48s
CI / Build container images (push) Has been skipped
Add editor section node; serialize pnpm CI jobs to fix runner flake
Two corrective changes:
- Register the `section` node in the web TipTap editor (with
  wrapInSection/unwrapSection commands) so the editor schema matches the
  shared editorSchema again. The schema-drift guard
  (document-extensions.test) rightly failed after 2e96173 added `section`
  to the shared schema without the editor side — this restores it.
- Replace the ineffective warm-up gate with a serial chain of the pnpm
  CI jobs (checks -> auth-e2e -> fidelity). Warming the shared action
  cache did not help: the dependent jobs still started together and
  corrupted the cache during concurrent extraction. Serializing them is
  what actually prevents it; CD image builds still run in parallel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 10:08:41 +02:00

211 lines
5.5 KiB
TypeScript

import { Node, nodeInputRule, textblockTypeInputRule, wrappingInputRule } from '@tiptap/core';
import { attributesFromSpec, nodeSpec, passthroughNodeIO } from '../spec-utils';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
documentTextBasics: {
setParagraph: () => ReturnType;
setHeading: (level: 1 | 2 | 3 | 4) => ReturnType;
toggleHeading: (level: 1 | 2 | 3 | 4) => ReturnType;
toggleBlockquote: () => ReturnType;
toggleCodeBlock: () => ReturnType;
setHorizontalRule: () => ReturnType;
setHardBreak: () => ReturnType;
};
documentSection: {
/** Wrap the selection in a styled section (issue #75). */
wrapInSection: (attrs: { pluginId: string; styleId: string }) => ReturnType;
/** Unwrap the nearest section, leaving its content in place. */
unwrapSection: () => ReturnType;
};
}
}
/** ProseMirror's document root; `topNode` is a schema-construction option,
* not part of the NodeSpec, so it is not derived from `nodeSpec('doc')`. */
export const Doc = Node.create({
name: 'doc',
topNode: true,
content: nodeSpec('doc').content,
});
export const Text = Node.create({
name: 'text',
group: nodeSpec('text').group,
});
const paragraphSpec = nodeSpec('paragraph');
export const Paragraph = Node.create({
name: 'paragraph',
group: paragraphSpec.group,
content: paragraphSpec.content,
...passthroughNodeIO(paragraphSpec),
addCommands() {
return {
setParagraph:
() =>
({ commands }) =>
commands.setNode(this.name),
};
},
addKeyboardShortcuts() {
return { 'Mod-Alt-0': () => this.editor.commands.setParagraph() };
},
});
const headingSpec = nodeSpec('heading');
const HEADING_LEVELS = [1, 2, 3, 4] as const;
export const Heading = Node.create({
name: 'heading',
group: headingSpec.group,
content: headingSpec.content,
defining: headingSpec.defining,
addAttributes() {
return attributesFromSpec(headingSpec);
},
...passthroughNodeIO(headingSpec),
addCommands() {
return {
setHeading:
(level: 1 | 2 | 3 | 4) =>
({ commands }) =>
commands.setNode(this.name, { level }),
toggleHeading:
(level: 1 | 2 | 3 | 4) =>
({ commands }) =>
commands.toggleNode(this.name, 'paragraph', { level }),
};
},
addKeyboardShortcuts() {
return Object.fromEntries(
HEADING_LEVELS.map((level) => [
`Mod-Alt-${level}`,
() => this.editor.commands.toggleHeading(level),
]),
);
},
addInputRules() {
return HEADING_LEVELS.map((level) =>
textblockTypeInputRule({
find: new RegExp(`^(#{${level}})\\s$`),
type: this.type,
getAttributes: { level },
}),
);
},
});
const blockquoteSpec = nodeSpec('blockquote');
export const Blockquote = Node.create({
name: 'blockquote',
group: blockquoteSpec.group,
content: blockquoteSpec.content,
...passthroughNodeIO(blockquoteSpec),
addCommands() {
return {
toggleBlockquote:
() =>
({ commands }) =>
commands.toggleWrap(this.name),
};
},
addKeyboardShortcuts() {
return { 'Mod-Shift-b': () => this.editor.commands.toggleBlockquote() };
},
addInputRules() {
return [wrappingInputRule({ find: /^\s*>\s$/, type: this.type })];
},
});
const sectionSpec = nodeSpec('section');
export const Section = Node.create({
name: 'section',
group: sectionSpec.group,
content: sectionSpec.content,
defining: sectionSpec.defining,
addAttributes() {
return attributesFromSpec(sectionSpec);
},
...passthroughNodeIO(sectionSpec),
addCommands() {
return {
wrapInSection:
(attrs) =>
({ commands }) =>
commands.wrapIn(this.name, attrs),
unwrapSection:
() =>
({ commands }) =>
commands.lift(this.name),
};
},
});
const codeBlockSpec = nodeSpec('code_block');
export const CodeBlock = Node.create({
name: 'code_block',
group: codeBlockSpec.group,
content: codeBlockSpec.content,
marks: codeBlockSpec.marks,
code: codeBlockSpec.code,
defining: codeBlockSpec.defining,
whitespace: codeBlockSpec.whitespace,
...passthroughNodeIO(codeBlockSpec),
addCommands() {
return {
toggleCodeBlock:
() =>
({ commands }) =>
commands.toggleNode(this.name, 'paragraph'),
};
},
addKeyboardShortcuts() {
return { 'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock() };
},
addInputRules() {
return [textblockTypeInputRule({ find: /^```$/, type: this.type })];
},
});
const horizontalRuleSpec = nodeSpec('horizontal_rule');
export const HorizontalRule = Node.create({
name: 'horizontal_rule',
group: horizontalRuleSpec.group,
...passthroughNodeIO(horizontalRuleSpec),
addCommands() {
return {
setHorizontalRule:
() =>
({ chain }) =>
chain().insertContent({ type: this.name }).run(),
};
},
addInputRules() {
return [nodeInputRule({ find: /^---$/, type: this.type })];
},
});
const hardBreakSpec = nodeSpec('hard_break');
export const HardBreak = Node.create({
name: 'hard_break',
group: hardBreakSpec.group,
inline: hardBreakSpec.inline,
selectable: hardBreakSpec.selectable,
...passthroughNodeIO(hardBreakSpec),
addCommands() {
return {
setHardBreak:
() =>
({ chain }) =>
chain().insertContent({ type: this.name }).run(),
};
},
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => this.editor.commands.setHardBreak(),
};
},
});