@arrisa/schema
v1.0.29
Published
Arrisa — schema
Readme
@arrisa/schema
Ready-made Arrisa editor schema extensions — document roots, blocks, lists, marks, links, colors, and images wired with menus, keymaps, and input rules on top of @arrisa/types.
This package is editor chrome, not the low-level Schema class (that lives in @arrisa/doc). Node/mark types come from @arrisa/types; factories here register those types and attach UI behavior.
Install
pnpm add @arrisa/schema @arrisa/editor @arrisa/state @arrisa/doc
# or npm / yarnPeer-style stack used by the factories: @arrisa/command, @arrisa/history, @arrisa/phrases, @arrisa/types (installed as package dependencies).
Quick start
Document editor with a lightweight preset:
import {Arrisa, menuBar} from "@arrisa/editor"
import {basicSchema} from "@arrisa/schema"
import {history} from "@arrisa/history"
let parent = document.getElementById("editor")!
let editor = Arrisa.create({
parent,
config: [basicSchema(), history(), menuBar()],
})
editor.focus()Full rich-text (blocks, lists, marks, media, resize):
import {fullSchema} from "@arrisa/schema"
let editor = Arrisa.create({
parent,
config: [fullSchema(), history(), menuBar()],
})Compose factories instead of a preset:
import {
blockDoc,
paragraph,
heading,
bulletList,
strong,
emphasis,
link,
} from "@arrisa/schema"
let config = [
blockDoc(),
paragraph(),
heading(),
bulletList(),
strong(),
emphasis(),
link(),
]Features / concepts
| Concept | Role |
|---------|------|
| Presets | basicSchema, fullSchema, inlineSchema, composeSchema, messengerSchema assemble extensions for common hosts |
| Factories | paragraph(), strong(), image(), … each returns an EditorState.Extension (or array) |
| Namespaces | Merged namespaces expose pieces for re-ranking/omission: paragraph.button, link.tooltip, image.dropHandler, … |
| Types vs chrome | Schema elements from @arrisa/types; this package adds menu, keys, input rules, dialogs, themes |
Presets
| Preset | Typical use |
|--------|-------------|
| basicSchema() | Block Doc, paragraphs, headings, line breaks, strong/em/link |
| inlineSchema() | InlineDoc, basic marks, inline images, line breaks |
| composeSchema(config?) | Block chat field: Doc, paragraphs, lists, quotes, code blocks, compose marks (no spoiler/headings/media) |
| messengerSchema(config?) | Inline/spoiler chat: InlineDoc + marks (default); codeBlocks: true → block Doc + CodeBlock |
| fullSchema() | Blocks, lists, quotes, HR, full inline marks, image/figure, image resize |
Mark bundles (usable alone or inside presets):
basicMarks()— strong, emphasis, linkcomposeMarks()— strong, em, underline, strike, code, link, clear-formatting button (no spoiler)messengerMarks()— spoiler, strong, em, underline, strike, code, linkinlineMarks()— basic + code, underline, strike, spoiler, super/sub, text/background color
Input rules (selection)
- Headings:
#…###### - Code block:
```or```langthen space (language →CodeBlockLanguage) - Blockquote:
> - Horizontal rule:
---on an empty textblock - Bullet list: optional space +
- - Ordered list: optional space +
N.
Shortcuts (selection)
| Binding | Action | |---------|--------| | Mod-b / Mod-i / Mod-u / Mod-/ / Mod-` | Strong / em / underline / strike / code | | Mod-Shift-p | Spoiler | | Mod-. / Mod-, | Super / subscript | | Mod-k | Toggle link (floating prompt when none selected) | | Ctrl-Shift-0…6 | Paragraph / heading levels | | Ctrl-Shift-\ | Code block | | Mod-Shift-l/r/e | Align left / right / center | | Ctrl-Alt-i (Mac: Ctrl-Cmd-i) | Insert/update image dialog |
Main public API
Document roots and blocks
import {
blockDoc,
inlineDoc,
paragraph,
heading,
codeBlock,
alignment,
direction,
blockquote,
horizontalRule,
lineBreak,
} from "@arrisa/schema"blockDoc()/inlineDoc()— registerDocorInlineDocparagraph(),heading(),codeBlock()— textblocks + chrome (codeBlockregistersCodeBlock+CodeBlockLanguage, fence input rule, theme, and token colors). Tab inside a code block inserts four spaces; Shift-Tab removes up to four spaces before the caret or at the start of the line; elsewhere Tab moves focus.alignment(),direction()— textblock marks + menu submenusblockquote(),horizontalRule()— structure + input ruleslineBreak()— hard break leaf (br)
Lists
import {bulletList, orderedList} from "@arrisa/schema"
bulletList() // block list items (default)
bulletList({blockItems: false}) // inline-only list items
orderedList()Marks
import {
strong,
emphasis,
code,
underline,
strikethrough,
spoiler,
superscript,
subscript,
basicMarks,
composeMarks,
messengerMarks,
inlineMarks,
clearFormattingButton,
} from "@arrisa/schema"Link
import {
link,
isLinkPasteUrl,
type LinkConfig,
type LinkPrompt,
type LinkPromptRequest,
} from "@arrisa/schema"
link() // mark + Mod-k + cursor tooltip + paste URL over selection or at caret
link({prompt: false}) // remove-only (no add-link UI)
let prompt: LinkPrompt = (req: LinkPromptRequest) => {
// host UI; req.apply sanitizes via applyLink — never raw Link.of
req.apply("https://example.com")
}
let linkConf: LinkConfig = {prompt}
link(linkConf)
if (isLinkPasteUrl(text)) {
// absolute http(s) / mailto / xmpp, no spaces
}Pasting a lone absolute URL (isLinkPasteUrl) at an empty caret inserts the URL as a Link; a non-empty selection is wrapped and the URL string is not inserted. HTML <a href> paste is unchanged.
Default add-link UI is a floating tooltip form (prompt: "floating"), not a Dialog panel. The default toggle removes existing links or opens add UI; it does not prefill href (remove-first). cancel dismisses the built-in floating field and focuses the editor; custom prompts still own their DOM. req.apply / form submit run applyLink (sanitizeLinkHref). The read-only cursor-inside-link tooltip is unchanged.
Color
import {color, backgroundColor, ColorPicker} from "@arrisa/schema"
color()
backgroundColor()
// Optional palette layout (facets on ColorPicker)
// ColorPicker.options / ColorPicker.widthValues must pass isSafeCssColor from @arrisa/types (defense in depth in the menu control).
Image / figure
import {
image,
figure,
imageResizing,
imageUploader,
activeImage,
insertImage,
imageDialog,
} from "@arrisa/schema"
// Register types + insert chrome
image()
figure({captioned: true})
imageResizing()
// Provide an upload handler for file picker / drop
imageUploader.of(async (file, editor, progress) => {
progress(50)
return await uploadToCdn(file)
})image()— inlineImage+ dialog, menu, drop handlerfigure({captioned?})— blockFigure, optionallyCaptionedFigureimageResizing()—ImageSizemark, drag handle, enlarge/shrink keysinsertImage/imageDialog— open/close insert panelactiveImage(sel)— active image/figure tag at selection, if any
Presets and messenger config
import {
basicSchema,
fullSchema,
inlineSchema,
composeSchema,
messengerSchema,
type ComposeSchemaConfig,
type MessengerSchemaConfig,
} from "@arrisa/schema"
composeSchema({
exclusivity: "none", // | "code-strike" | {isolating: Mark.Type[]}
link: {prompt: "floating"}, // | false | (req) => { req.apply(href) }
})
messengerSchema({
exclusivity: "none", // | "code-strike" | {isolating: Mark.Type[]}
codeBlocks: false, // true → Doc + paragraphs + CodeBlock (fence rule + language)
})messengerSchema remains the inline/spoiler path. Use composeSchema for a block chat field (lists, quotes, fenced code, no spoiler).
Chat field hosts should prefer @arrisa/message’s composeField (block lists/quote/code) or messengerCompose (inline/spoiler).
Code token roles
import {highlightCode, type CodeRole, type CodeSpan} from "@arrisa/schema"
let spans = highlightCode(source, "ts")highlightCode(text, language?) returns CodeSpan[]. from and to are UTF-16 indexes into text (from inclusive, to exclusive).
Spans are a view. They are not part of PreEntity or the dump.
Blocks over 32_000 UTF-16 units or 400 lines return [].
Roles: kw-flow, kw-decl, modifier, type, function, property, string, interpolation, escape, number, constant, comment, directive, tag, attribute, regex.
Doc comments set emphasis: "bold". Other spans omit emphasis.
Language ids are case-folded. Aliases: typescript → ts, javascript → js, python → py, kotlin → kt, yml → yaml. A missing, empty, or unknown language does not add keywords.
codeBlock() paints those spans on the fenced block. Each token is a decoration with classes arrisa-tok and arrisa-tok-<role>. Doc comments (emphasis: "bold") also get arrisa-tok-bold. Inline code marks are not tokenized. Painting does not change dump text or pre offsets.
Colors use two layers. Set --arrisa-code-<name> (no -default) on an ancestor to override a role. Scheme defaults are --arrisa-code-<name>-default, chosen by Arrisa.colorScheme (&light / &dark). Neither path re-lexes. Names: violet, indigo, lavender, sky, blue, cyan, green, lime, mint, peach, orange, seagreen, coral, red, yellow, pink.
Layering / related packages
@arrisa/types → @arrisa/schema → host app
↑
@arrisa/editor, @arrisa/command, …| Package | Relationship |
|---------|----------------|
| @arrisa/types | Schema elements (Paragraph, Strong, Link, …) |
| @arrisa/doc | Low-level Schema, document tree — not this package |
| @arrisa/editor | View: menus, input rules, dialogs, themes |
| @arrisa/command | Commands (toggleMark, setTextblockType, …) and menu model |
| @arrisa/message | Messenger compose preset on top of schema marks |
| @arrisa/table | Table chrome; combine with fullSchema() / custom blocks |
Security
Schema registration is not an XSS boundary. Untrusted HTML paste/load needs app sanitization (Arrisa.htmlSanitize / sanitizeHTML). Link and image helpers reject dangerous schemes (javascript:, unsafe data:, etc.). See SECURITY.md.
License
MIT © inshinrei
