Better Rich Text Buy licence

Quality tools

On this page
  1. Accessibility checker
  2. Rules
  3. Issues
  4. Panel
  5. Commands
  6. Custom rules
  7. On the server
  8. Link checker
  9. Reachability
  10. Panel and decorations
  11. Visual aids

Three opt-in tools help authors publish content that works for everyone: an accessibility checker, a link checker and visual aids that show block structure and invisible characters. Each is its own import, so an editor that does not use one does not bundle it. apps/playground/quality.html shows all three on a document with seeded problems.

TypeScript
import { createRichTextEditor, standardPreset } from '@better-rich-text/core';
import { visualAidsFeature } from '@better-rich-text/ui';
import { createA11yCheckPlugin } from '@better-rich-text/plugin-a11y-check';
import { createLinkCheckPlugin } from '@better-rich-text/plugin-link-check';

createRichTextEditor('#editor', {
  plugins: [createA11yCheckPlugin(), createLinkCheckPlugin({ checker: checkOnMyServer })],
  toolbar: `${standardPreset.toolbar} | a11yCheck linkCheck visualBlocks visualChars`,
  features: [...standardPreset.features, visualAidsFeature()],
});

Accessibility checker

@better-rich-text/plugin-a11y-check checks the document after every edit (once typing pauses, debounceMs, default 400 ms). The a11yCheck toolbar button shows the number of errors and warnings and opens the issues panel below the content.

Rules

RuleSeverityFindsFixes
image-alt error Images with no alternative text Add text; mark decorative
image-decorative-link error A linked image marked decorative, so the link has no name Add text
image-alt-quality warning Alternative text that is a file name or a generic word; very long text (notice) Rewrite
link-text warning, error when empty "click here", "read more", empty links Rewrite text; remove link
heading-empty error Headings with no text Change to paragraph (safe)
heading-multiple-h1 warning More than one H1 Change to H2
heading-order warning Skipped levels (H2 then H4) Change to the next level
duplicate-anchor error The same anchor id twice Make unique (safe)
table-header error Tables with two or more rows and no header cells Make the first row a header
table-caption notice Tables with no caption Add caption
contrast error Coloured text or highlights under 4.5:1 (3:1 for large text) Use a nearby accessible colour (safe); remove colour or highlight
all-caps warning Runs of capitals with 24 or more letters Change to sentence case
fake-list warning Two or more paragraphs starting with •, -, *, 1. or a) Convert to a real list (safe)
document-language warning, error when invalid No document language Set language
media-alternatives warning Video without captions; embeds without a title Add title

Contrast uses the text colour, highlight and table cell shading in the document against the editing surface's colours, which the plugin reads from the page. If a colour cannot be resolved exactly (unknown background, transparency, a keyword it does not know) the result is a notice asking for a manual check. It never reports a pass it could not measure. Large text is 18pt, or 14pt bold, from the font size mark; H1 to H3 count as large.

Issues

Each issue has a stable id (rule:nodeId), severity, category, message, help, the nodeId and its path from the root, a text range where it is about part of a block, evidence (for example the measured ratio and colours), manual for results a person has to judge, and fixes. Ids survive unrelated edits, and an issue disappears as soon as its content is repaired.

Panel

The panel lists issues in document order with a text severity label (Error, Warning, Review), never colour alone. It filters by severity, announces result changes through a polite live region and works from the keyboard; Escape closes it and returns focus to the editor. Selecting a message selects the content in the document and outlines it. Fixes that need author input (alternative text, link text, captions, the document language) open a small form. "Fix all safe issues" applies only fixes marked deterministic, as one undo step. "Ignore" hides an issue until its content changes.

The panel can also be mounted anywhere with new A11yPanel({ editor, view, mount, controller }).

Commands

CommandInputResult
a11y.check none Counts and issue summaries
a11y.applyFix { issueId, fixId, value? } The fixed issue; stale-target if the issue no longer exists
a11y.fixAll none Ids of the issues fixed
a11y.ignore { issueId, reason? }
a11y.select { issueId } Selects the issue's content
a11y.next { backward? } Selects the next issue (Alt+Shift+F9)

Fixes are typed transaction edits: they are undoable, replayed in collaboration like any command, and re-validated against the current document before they run.

Custom rules

The a11y.rules filter receives the rule list; a11y.issues receives the results; a11y.checked runs after each check.

TypeScript
import type { A11yRule } from '@better-rich-text/plugin-a11y-check';

const noPlaceholder: A11yRule = {
  id: 'no-lorem',
  category: 'text',
  severity: 'warning',
  description: 'Placeholder text left in the document',
  *check(ctx) {
    for (const leaf of ctx.tree.textLeaves()) {
      if (leaf.text.includes('Lorem ipsum')) yield { nodeId: ctx.tree.parentId(leaf.id)!, message: 'Placeholder text left in' };
    }
  },
};

editor.editor.hooks.addFilter('a11y.rules', (rules) => [...rules, noPlaceholder]);

Options: disable (rule ids), rules (replace the set), colors, ambiguousLinkText, allCapsMinLetters, suggestedLanguage, debounceMs, checkOnLoad, resolveColors and saved ignores.

On the server

The same rules run without a DOM:

TypeScript
import { audit } from '@better-rich-text/plugin-a11y-check/audit';

const report = audit(envelope, schema, { colors: { text: '#1f1f1f', background: '#ffffff' } });
if (report.counts.error > 0) rejectPublish(report.issues);

Every report carries a note that automated checks find only some problems. A clean report is not proof that content is accessible.

@better-rich-text/plugin-link-check checks every link after each edit. Without any setup it validates destinations offline:

  • unsafe: javascript:, vbscript:, data: (other than images), file: and any scheme outside the allowed list, including versions hidden with whitespace, entities or control characters.

  • invalid: malformed URLs, bad percent-encoding, spaces, invalid host names, email addresses and phone numbers.

  • empty: no destination, or just #.

  • missing-anchor: #name links with no matching anchor in the document. Add anchors from elsewhere with the linkCheck.anchors filter.

  • duplicate-text: the same link text pointing to different places.

Reachability

The editor never fetches links itself. Pass a checker that asks your server, which can apply network policy, redirect limits and response size limits:

TypeScript
createLinkCheckPlugin({
  checker: async (url, { signal }) => {
    const response = await fetch(`/api/check-link?url=${encodeURIComponent(url)}`, { signal });
    return response.json(); // { status: 'valid' | 'missing' | ..., httpStatus?, message? }
  },
});

Statuses are valid, missing, unauthorized, unreachable, blocked, timeout, rateLimited, unsupported and unknown. httpStatusToLinkStatus maps HTTP codes: 401 and 403 are unauthorized, not missing. Each destination is checked once and cached; changing a link's URL checks the new one. Checks run at most concurrency (4) at a time, time out after timeoutMs (10 s) and are cancelled when the editor is destroyed. Relative links are checked only when you give a baseUrl. sameOriginChecker({ origin }) is a ready-made checker for links to your own site; it reports other origins as unsupported.

Results never change a destination by themselves.

Panel and decorations

Problem links get a wavy (broken) or dotted (warning) underline through the CSS Custom Highlight API, so the content DOM is untouched. The linkCheck toolbar button shows the problem count and opens a panel with each link's status, text and address, and the actions show, edit, open (safe http and https links only, in a new tab without opener), unlink, retry and ignore.

Commands: linkCheck.check { force? }, linkCheck.select, linkCheck.unlink, linkCheck.retry and linkCheck.ignore, each taking { linkId }. Hooks: linkCheck.anchors, linkCheck.results and linkCheck.checked.

Visual aids

visualAidsFeature() from @better-rich-text/ui adds two toolbar toggles:

  • visualBlocks outlines block elements and labels each with its tag (P, H2, LI, TD, ...).

  • visualChars shows invisible characters: spaces ·, non-breaking spaces °, tabs →, line breaks ↵, paragraph ends , soft hyphens ¬, zero-width characters ¦ and direction controls ⇄ (zero-width and direction controls show their code point on hover).

Both are view-only. Block outlines are a class on the editable element plus CSS. Characters are drawn in an overlay beside the content, positioned from the rendered text and computed from the document model. Nothing is added to the document, its HTML, the clipboard or any export. The overlay redraws after edits and resizes, and stops after 5,000 glyphs (limit).

Use them without the toolbar through visualAidsFor(view), which returns an object with setBlocks, setCharacters, toggleBlocks, toggleCharacters and subscribe. findInvisibles(schema, root) lists the invisible characters of a document without a DOM.