Embedded content and editorial review

Documentation index

The browser editor and WinForms control support atomic embedded-content chips, multiple suggestion triggers, contextual host actions, comment threads and tracked editorial changes. The browser and ASP.NET samples share these features; the WinForms sample exposes the same contracts through typed methods and events.

Try the samples

Choose Editorial demo in the browser, ASP.NET or WinForms sample. The article includes an Upcoming Events textblock, a code snippet, an image and an initial editorial comment. Tracking starts enabled.

Image treatments and snippet formatting in these samples are deterministic demonstrations. They do not call an AI provider or implement official CODE Magazine production standards. The host supplies its real formatter, brand instructions, AI service, permissions and asset storage.

Host suggestions for reusable textblocks

Atomic textblock, code toolbar, comments and tracked edits

These are actual WebView2 captures of the generated browser sample. The native/browser verification program regenerates them after its rendered checks pass.

Atomic chips

editor.registerTokens([
  { id: "eventlist", type: "textblock", value: "##eventlist##", label: "Upcoming Events" }
]);

The stored value remains ##eventlist##; the label is a rendering choice. Recognized tokens are protected atomic ranges in the model and noneditable chips in Visual view. Caret movement cannot enter their text, partial selections expand to the whole token, and Backspace/Delete remove one item. Copy exports the literal value, and undo restores it. Tokens can be recognized in loaded content or as the final delimiter is typed. Source view keeps ordinary literal text. Inline code, fenced code, raw fragments and images do not become chips.

IDs identify the referenced host object; block IDs and occurrence positions identify a particular use in the article. A host may reopen a definition without changing the article. Changing a shared definition belongs to the host's storage workflow and can affect other articles.

Registration replaces the current token catalog. A suggestion inserted as a token adds its definition to that catalog. Catalogs are host configuration, so register them again when reopening an article; unresolved values remain literal text rather than losing content.

Multiple suggestion providers

editor.configure({
  suggestions: [
    { id: "blocks", providerId: "content", trigger: "##" },
    { id: "mentions", trigger: "@" },
    { id: "attachments", trigger: "[[" }
  ],
  onSuggestions: async request => {
    // request includes registrationId, providerId, trigger, query, from/to,
    // requestId, revision, generation, and an AbortSignal.
    return [{
      id: "eventlist", label: "Upcoming Events",
      displayHtml: "<strong>Upcoming Events</strong><small>Shared textblock</small>",
      insertText: "##eventlist##", insertAs: "token", type: "textblock"
    }];
  }
});

registerSuggestions(registrations) replaces all registrations; unregisterSuggestion(id) removes one at runtime. Each registration has a unique ID, a trigger, optional provider ID, wordBoundary (default true), allowSpaces (false), minQueryLength (0) and maxQueryLength (100). Overlapping triggers prefer the longest match. Multiple triggers may share a provider. Registrations apply in Source and Visual views, with Visual code excluded. Suggestions are suspended during IME composition and for noncollapsed selections.

Hosts may use events instead of the callback: handle suggestionsRequested, then call provideSuggestions(requestId, items). suggestionsCancelled identifies requests that no longer apply. Responses to obsolete revisions/generations are ignored. There are at most 200 results per response. Selecting a suggestion replaces its trigger and query in one undoable edit.

Items support insertAs: "text", "token", or "image". Image items use src and alt: Visual view inserts a structural image; Source view inserts Markdown/HTML image syntax (or the supplied text/URL for plain text). label is the accessible plain-text fallback. HTML snippets accept a safe display subset: strong/bold, emphasis/italic, small, span/div/paragraph, line breaks, code, strike/underline and permitted image thumbnails. Scripts, active elements, event handlers and arbitrary attributes are excluded. Image loading follows the editor's image-origin policy. Use host CSS on the popup container for styling rather than interactive HTML controls inside an option.

Context actions on chips, code and images

editor.configure({
  contextActions: [
    { id: "definition", label: "Open definition", targets: ["token"], scope: "reference" },
    { id: "format", label: "Format snippet", targets: ["code"] },
    { id: "pencil", label: "Create pencil drawing", targets: ["image"] }
  ],
  onContextAction: async request => {
    request.reportProgress("Formatting snippet…");
    const code = await hostFormatter(request.context.code, request.context.language, request.signal);
    return { kind: "replaceCode", code, language: request.context.language };
  }
});

The editor owns toolbar placement, popup keyboard navigation, result preview and undo. codeLanguages configures the code toolbar dropdown; setCodeLanguage(blockId, language) updates the stored fence language in one transaction. Toolbars and menus never become saved document content or clipboard text.

Actions identify a stable block, the current code/language or image metadata, or a token's reference and occurrence. scope defaults to occurrence; use reference for host definition actions. For hosts using events, handle contextActionRequested, update progress with reportContextActionProgress(id, message), and return a result through completeContextAction(id, result).

Result Fields Effect after Apply
Handled host interaction handled: true No article edit; for example, open a definition window
Code replacement kind: "replaceCode", code, optional language Replace snippet in one transaction
Image replacement kind: "replaceImage", src, optional alt Retain original asset in undo/review history; replace this occurrence
Token replacement kind: "replaceToken", value Replace the occurrence's stored text
Failure error Display an error without editing the article

applyContextAction(id) applies a previewed result; cancelContextAction(id) cancels work and raises contextActionCancelled. Callback requests include an AbortSignal. Late cancelled results are ignored. Apply checks the document generation and target signature; edited/removed targets are never overwritten. Stale results remain visible for inspection. Other unrelated blocks may be edited while work is in progress. An applied replacement is also a tracked proposal when tracking is enabled. Asset deletion is the host's responsibility; replacing an image does not delete its original file.

Comments and tracked changes

editor.configureReview({ author: { id: "editor-1", name: "Magazine Editor" }, tracking: true });
const thread = editor.addComment({ text: "Please expand this explanation.", from: 20, to: 48 });
editor.replyToComment(thread.id, "Added an example.");
editor.resolveComment(thread.id, true);
editor.showReview(true);
editor.setReviewView("markup");

Comment options support scope: "range" (default), "block" or "article". Offsets use the editor's current UTF-16 model text, matching the selection API. Visual formatting delimiters are not part of that text; an image occupies one object character and a chip retains its literal value length. A thread stores replies, authors, UTC timestamps, resolution and an anchor containing block identity, range, quotation, surrounding context and revision.

Document operations map attached comments as text changes. Removing an entire commented passage marks it needsReattachment; review UI never silently jumps it to unrelated text. reattachComment(id, from, to) or Attach to selection reconnects it. Resolving a thread keeps its history. Comments, replies, resolutions and anchor changes participate in undo and dirty state even when publication text is unchanged.

Tracking records operation-sized proposals with author, timestamp, label, removed/inserted text, structural before/after information and a canonical publication-content patch. This covers text edits, formatting, headings/lists/tables, language changes and image replacement. Undo/redo restores both document and corresponding review metadata. Code and image action replacements use the same transaction path.

acceptChange(id) keeps a proposal; rejectChange(id) reverses it. Rich structural proposals are grouped by affected block: accepting a later edit also accepts earlier pending edits incorporated into that block. Reject later dependent changes first. This prevents rejecting an earlier replacement from silently destroying later work. The UI reports dependency conflicts. This is an operation/block review model, not Word's full character-level merge engine.

There is a limit of 500 review proposals per article; resolve changes and start a clean editorial revision for longer cycles. The runtime is intended for bounded article-sized documents, not unbounded document histories or simultaneous collaborative editing.

Save Markdown with its companion

const snapshot = editor.getSnapshot();
const article = await editor.getReviewPackage();
// article = { markdown, review }
await host.saveArticleAtomically(article);
editor.markSaved(snapshot.revision, snapshot.sessionId);

await editor.loadReviewPackage(article);
const publicationMarkdown = editor.exportPublication();

The package contains clean proposed Markdown and a version-1 review sidecar. The sidecar stores a stable documentId, unique save revisionId, SHA-256 contentHash, current model/format/view, comments and proposals. The model copy preserves rich block identity. The same mechanism supports HTML/plain content when the corresponding format is selected; the package's markdown property is the publication-content field for all formats.

Store the package as one .article.json file/database record, or split its two fields into article.md and article.review.json and save them as one revision. The SHA-256 pairing detects mixed revisions; it is an integrity check, not a digital signature or an authorization system. Do not save only getValue() during editorial work. Plain .md remains a valid initial author submission and a clean publication/export format, but cannot carry the review conversation by itself.

getReviewState() returns a copy of live review metadata. reviewChange notifies hosts to refresh review controls. A review package is a committed snapshot; later edits remain dirty and a stale save acknowledgement cannot clear them. Load checks integrity before replacing the current article and rejects concurrent changes during validation.

External Markdown edits

By default, loading a mismatched Markdown/sidecar pair fails. The host must explicitly request reconciliation:

await editor.loadReviewPackage(
  { markdown: externallyEditedMarkdown, review: previousSidecar },
  { reconcile: true, trackExternal: true }
);

Reconciliation tries the comment quotation plus surrounding context, then a unique quotation. Ambiguous/missing passages remain needsReattachment. Previous pending proposals become needsReconciliation and are retained as evidence rather than applied to a different document. With trackExternal, the imported difference becomes a proposal labeled with unverified external authorship. The editor cannot infer who changed a standalone Markdown file. Publication and review storage still belong to the host; this is not a multi-user merge or CRDT system.

WinForms host API

await editor.ConfigureExtensionsAsync(new(
    Tokens: [new("eventlist", "##eventlist##", "Upcoming Events")],
    Suggestions: [new("blocks", "##"), new("mentions", "@")],
    ContextActions: [new("format", "Format snippet", ["code"])]));

editor.SuggestionsRequested += (_, args) =>
    args.Result = LookupSuggestionsAsync(args.Request, args.CancellationToken);

editor.ContextActionRequested += (_, args) =>
    args.Result = FormatOrTransformAsync(args.Request, args.CancellationToken, args.ReportProgress);

await editor.ConfigureReviewAsync(new(new("editor-1", "Magazine Editor"), true));
await editor.AddCommentAsync(new("Please clarify.", From: 20, To: 48));
await editor.ShowReviewAsync();
ReviewPackage article = await editor.GetReviewPackageAsync();

EditorExtensionOptions, EditorToken, SuggestionRegistration, SuggestionItem, ContextAction, ContextActionResult, ReviewAuthor, ReviewOptions, CommentOptions, ReviewPackage and ReviewLoadOptions are public types. Event handlers assign their asynchronous result task; cancellation follows trigger/context lifetime. Host callbacks are asynchronous notifications, so they can invoke editor APIs without waiting on a blocked UI request. Native application storage and windows remain ordinary host code.

Other typed methods include RegisterTokensAsync, RegisterSuggestionsAsync, UnregisterSuggestionAsync, SetContextActionsAsync, SetCodeLanguageAsync, BeginContextActionAsync, ApplyContextActionAsync, CancelContextActionAsync, GetReviewStateAsync, ReplyToCommentAsync, ResolveCommentAsync, ReattachCommentAsync, AcceptChangeAsync, RejectChangeAsync, SetReviewViewAsync, LoadReviewPackageAsync and ExportPublicationAsync. ReviewChanged reports sidecar changes. Use the normal WinForms UI thread for public control methods.