DITA XML WYSIWYG editor
A WYSIWYG DITA XML editor, made by customizing CKEditor 5 with CKEditor 5 Elements. It edits a Lightweight DITA topic visually, and the data of the editor is the DITA XML of the topic, in its XML form, XDITA: editor.getData() returns it, and editor.setData() loads one. The content is its elements, each by its DITA name: a topic has its title, its shortdesc and its body, whose sections hold ps, lists and notes, and a b is an element in a line of text.
The XML below the editor is its data, as it changes: edit the topic and see it follow, or press Source to edit the XML itself.
- Right-click an element, or press Shift+F10: its menu, to insert, move, copy or delete it. The breadcrumbs show the path of the DITA elements at the cursor, e.g.
body › section › ul › li › p, and open the same menu. - Alt+Enter: quick insert, the elements that DITA allows at the cursor: a list item in a list, a section in the body.
- Enter in a list item makes the next item, and leaves the list in an empty one. In a
pre, it inserts a new line, and leaves it on an empty last line. - Title, Short description, Section, the two lists, Note, Preformatted text, Bold, Italic and Underline are the commands of the elements, by their kind.
- A section or a list item has a floating toolbar, to move, duplicate or delete it.
- Click a cross-reference: a modal with its attributes, the
href.
Run it as a page of its own, with the XML below the editor: pnpm example:dita.
Why CKEditor 5 Elements for DITA XML
- The editor is element first, as DITA is. Each DITA element is an element of the editor, with its name and its attributes;
b,iandphare elements too, not marks on the text. What you edit is the tree of the topic. - The XML round-trips. The topic is saved as it is loaded, element for element, with its
id: no HTML in between, no fillers. - The DITA elements are a JSON file. The element schema declares what each one holds and where it goes: sections in the body, list items only in lists,
prekeeping its whitespace. - It is CKEditor 5. Its undo, its clipboard and its source view work as usual, with the element features: menus, toolbars and breadcrumbs of the elements.
How it is made
The example is the ckeditor5-elements-dita-example package: an element schema, the config of the editor, and the topic. Everything else is CKEditor 5 Elements as it is, and the XML of the DocBook example, which this one shares.
The elements: an element schema
Each DITA element is an element of the element schema, with its DITA name. The p is CKEditor's paragraph, so typing, Enter and an empty editor work as usual.
schema.json
{
"title": { "type": "block", "allowIn": ["$root", "section"] },
"shortdesc": { "type": "block", "allowIn": ["$root"] },
"body": { "type": "container", "allowIn": ["$root"] },
"section": { "type": "container", "allowIn": ["body"], "editingTag": "section" },
"ul": { "type": "container", "listItem": "li", "allowIn": ["body", "section", "li", "note"], "editingTag": "ul" },
"ol": { "type": "container", "listItem": "li", "allowIn": ["body", "section", "li", "note"], "editingTag": "ol" },
"li": { "type": "container", "editingTag": "li" },
"note": {
"type": "container",
"allowIn": ["body", "section", "li"],
"allowAttributes": ["type"],
"editingTag": "aside"
},
"pre": { "type": "block", "verbatim": true, "allowIn": ["body", "section", "li", "note"] },
"b": { "type": "block", "isInline": true, "editingTag": "b" },
"i": { "type": "block", "isInline": true, "editingTag": "i" },
"u": { "type": "block", "isInline": true, "editingTag": "u" },
"ph": { "type": "block", "isInline": true },
"xref": { "type": "block", "isInline": true, "allowAttributes": ["href"], "editingTag": "a" }
}- The
body, asection, a list item and anotearecontainers: they hold blocks. Atitle, ashortdesc, apreand the paragraph areblocks: they hold text.b,i,u,phandxrefare inlineblocks: elements in a line of text, which hold text. ulandolare lists oflis (listItem): they hold only list items, which are only in lists, and Enter makes the next item. Their commands make lists of the blocks, and switch one into the other.preis verbatim: it keeps its whitespace, in the data too, and Enter inserts a new line in it.allowInkeeps the parts of the topic where DITA has them: thetitlein the topic and in sections, theshortdescand thebodyin the topic, the sections in the body, and the lists, the notes andprein the body, sections, list items and notes.editingTagmakes the editing view HTML that the browser edits well:ulandlifor the lists,asidefor a note,afor a cross-reference. It is the editing view only: the data is DITA.
The data: XML
The data is the XML of the DocBook example, configured for a topic: its root element is the topic, the paragraph is p, and the prolog has the document type of a Lightweight DITA topic. The converters of the schema write the DITA names ('names'). The attributes of the topic are kept, e.g. its id, which DITA requires.
elementSchemaConverters: 'names',
xmlDocument: { root: 'topic', prolog: DITA_PROLOG, paragraph: 'p', className: 'dita' }The editor
config.ts
import {
Essentials,
IconBold,
IconBulletedList,
IconCodeBlock,
IconDocumentOutline,
IconHeading2,
IconInfo,
IconItalic,
IconNumberedList,
IconText,
IconUnderline,
Paragraph,
SourceEditing,
type EditorConfig
} from 'ckeditor5';
import {
ElementBreadcrumbs,
ElementActions,
ElementAttributes,
ElementClickable,
ElementClipboard,
ElementContextMenu,
ElementSchema,
ElementSchemaCommands,
ElementSelectable,
ElementToolbar,
ElementQuickInsert,
insertIcon,
pasteIcon,
type ElementMenuConfig
} from 'ckeditor5-elements';
import { XmlDocument } from 'ckeditor5-elements-xml-example';
import schema from './schema.json';
/**
* What comes before the topic: the XML declaration, and the document type of a Lightweight DITA topic (XDITA).
*/
export const DITA_PROLOG =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE topic PUBLIC "-//OASIS//DTD LIGHTWEIGHT DITA Topic//EN" "lw-topic.dtd">\n';
/**
* The menu of an element, for the context menu and the breadcrumbs: inserting, the element actions, the clipboard, its
* attributes, and deleting.
*/
export const ditaMenu: ElementMenuConfig = (element, editor) => [
{
title: 'Insert',
icon: insertIcon,
items: [editor.commands.get('quickInsertBefore'), editor.commands.get('quickInsertAfter')]
},
editor.commands.get('duplicateElement'),
'|',
editor.commands.get('moveElementUp'),
editor.commands.get('moveElementDown'),
'|',
editor.commands.get('copyElement'),
editor.commands.get('cutElement'),
{
title: 'Paste',
icon: pasteIcon,
items: [editor.commands.get('pasteElementBefore'), editor.commands.get('pasteElementAfter')]
},
'|',
editor.commands.get('showElementAttributes'),
editor.commands.get('deleteElement')
];
const ACTIONS = ['moveElementUp', 'moveElementDown', '|', 'duplicateElement', 'deleteElement'];
/**
* The editor config of the DITA example: the elements of `schema.json`, by their DITA names, with the data of a
* Lightweight DITA topic (see `XmlDocument`), and the features of `ckeditor5-elements`. Without `attachTo`, `licenseKey`
* and the data.
*/
export const ditaConfig: EditorConfig = {
plugins: [
Essentials,
SourceEditing,
Paragraph,
ElementSchema,
ElementSchemaCommands,
ElementSelectable,
XmlDocument,
ElementContextMenu,
ElementActions,
ElementClipboard,
ElementAttributes,
ElementClickable,
ElementToolbar,
ElementQuickInsert,
ElementBreadcrumbs
],
toolbar: {
items: [
'undo',
'redo',
'sourceEditing',
'|',
'title',
'shortdesc',
'section',
'|',
'ul',
'ol',
'note',
'pre',
'|',
'b',
'i',
'u',
// The path of the DITA elements at the cursor, on a toolbar line of its own.
'-',
'breadcrumbs'
],
shouldNotGroupWhenFull: true
},
// The elements of DITA, by their names in the data. Their editing view is HTML, from the converters of the schema.
elementSchema: schema,
elementSchemaConverters: 'names',
// The data: the XML of the topic, with the elements by their names, and CKEditor's paragraph as `p`.
xmlDocument: { root: 'topic', prolog: DITA_PROLOG, paragraph: 'p', className: 'dita' },
// A command and a button for each element, by its kind: the lists are lists, `pre` is verbatim.
elementSchemaCommands: {
title: { title: 'Title', icon: IconHeading2 },
shortdesc: { title: 'Short description', icon: IconText },
section: { title: 'Section', icon: IconDocumentOutline },
ul: { title: 'Bulleted list', icon: IconBulletedList },
ol: { title: 'Numbered list', icon: IconNumberedList },
note: { title: 'Note', icon: IconInfo },
pre: { title: 'Preformatted text', icon: IconCodeBlock },
b: { title: 'Bold', icon: IconBold, keystroke: 'Ctrl+B' },
i: { title: 'Italic', icon: IconItalic, keystroke: 'Ctrl+I' },
u: { title: 'Underline', icon: IconUnderline, keystroke: 'Ctrl+U' }
},
elementContextMenu: ditaMenu,
elementBreadcrumbs: { menu: ditaMenu },
// Click a cross-reference: a modal with its attributes, the `href`.
elementClickable: [
{
element: 'xref',
callback: (element, editor) => editor.commands.get('showElementAttributes')?.executeFor(element)
}
],
elementToolbar: [
{ element: 'section', items: ['section', '|', ...ACTIONS], ariaLabel: 'Section toolbar' },
{ element: 'li', items: ACTIONS, ariaLabel: 'List item toolbar' }
],
elementQuickInsert: {
labels: {
paragraph: 'Paragraph',
title: 'Title',
shortdesc: 'Short description',
body: 'Body',
section: 'Section',
ul: 'Bulleted list',
ol: 'Numbered list',
li: 'List item',
note: 'Note',
pre: 'Preformatted text',
b: 'Bold',
i: 'Italic',
u: 'Underline',
ph: 'Phrase',
softBreak: 'Line break'
},
// A cross-reference needs its target: write it in the source. A list is made of blocks, with its button.
exclude: ['xref', 'ul', 'ol']
}
};topic.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE topic PUBLIC "-//OASIS//DTD LIGHTWEIGHT DITA Topic//EN" "lw-topic.dtd">
<topic id="editing-dita">
<title>A topic in Lightweight DITA</title>
<shortdesc>This topic is <b>XDITA</b>: the editor loads it, and saves it as it is, with its <ph>id</ph>.</shortdesc>
<body>
<p>Each part of it is an element of the editor, with its DITA name: a <i>short description</i>, a <u>body</u>, its sections and their titles.</p>
<section>
<title>Lists and notes</title>
<ul>
<li>
<p>Put the cursor here: the breadcrumbs show the path of the DITA elements.</p>
</li>
<li>
<p>Press Enter at the end of an item for the next one, and in an empty one to leave the list.</p>
</li>
</ul>
<note type="tip">
<p>Right-click an element for its menu, or press Alt+Enter to insert an element where the cursor is.</p>
</note>
</section>
<section>
<title>Code and links</title>
<pre>const topic = editor.getData();
// The topic, as XDITA.</pre>
<p>A cross-reference keeps its target: <xref href="https://www.oasis-open.org/committees/dita/">DITA</xref>. Click it to see its attributes.</p>
</section>
</body>
</topic>What it does not do
- It is a part of Lightweight DITA: a topic with a title, a short description and a body of sections, paragraphs, lists, notes, preformatted text, highlighting, phrases and cross-references. Maps, figures, images, tables, definition lists and the other elements are not declared, so the editor does not keep them.
- The schema says where an element goes, not in which order or how many times. The editor does not require the
titleto be the first child of the topic, or a topic to have onebody; a paragraph of CKEditor can be out of the body, as the paragraph is not an element of the schema. - The target of a cross-reference is edited in the source. The editor shows it, and keeps it.
- Comments and processing instructions are not kept, and whitespace outside a
preis not either, as in most XML.