Skip to content

v1.13.0

Latest

Choose a tag to compare

@JackUait JackUait released this 03 Sep 21:15
· 384 commits to main since this release

Features

  • Live collaboration — Real-time multiplayer editing, turned on with collaboration: { doc }.
    • Editors on the same doc see each other's edits, carets and avatars, named by collaboration.user.
    • offline: true survives a reload by writing document content to browser storage.
    • Every tool must support read-only, because a collaboration editor boots read-only.
  • persistence, server and ticket — The document wiring that used to be yours to write.
    • persistence: { load, save } loads on mount and saves as the document changes, against your endpoint.
    • Saves are queued, never parallel, and only the newest pending document is sent.
    • server fills in the uploader and preview endpoints. ticket names the endpoint minting an access pass.
  • Documents convert outside the browserBlok.Server runs the editor's own serializer in-process.
    • AddBlokDocuments() registers it, and AddBlokServer already includes it.
    • IBlokDocumentConverter gives Markdown, HTML and plain text out, and Markdown in.
    • Markdown cannot express every block, so both directions report what changed.
  • Translate a document without handing a model its JSONextractTexts and injectTexts move the prose past a model.
    • The model never sees the structure, so it cannot drop ids or reorder blocks.
    • URLs, file names and code blocks are left out of the list.
    • A list whose length does not match the document throws rather than misplacing a translation.
  • Markdown out of a saved documentblocksToMarkdown is the synchronous, DOM-free twin of markdownToBlocks.
    • Headings become #, lists - or 1., and tables GFM pipe grids.
    • blocksToMarkdownWithReport also names every construct dropped or emitted lossily.
    • Markup written into Markdown comes back as literal text, since Blok has no raw-HTML block.
  • A heading keeps the anchor its links point atHeaderData.anchor renders as the heading's id.
    • It is captured from a pasted heading's own id, so in-document links survive a paste and a save.
    • restoreHeadingAnchors repairs documents already imported without it.
    • That pass reports every fragment it refused as no-match or ambiguous, rather than guessing.
  • An uploader can delete what it storeddelete?(url, ctx) is a third optional method on BlokUploader.
    • Blok sweeps the assets this session uploaded and then stopped using.
    • With the method absent nothing is ever deleted, which is what happened until now.
    • UploaderConfig.headers may now be a function, so a short-lived pass is minted per request.
  • Change a live document from outside the browserPOST /sync/{doc}/edit and /reset work over HTTP.
    • An edit inserts, updates or removes blocks, all-or-nothing, and reaches every open tab.
    • A reset reloads the document from your endpoint and tells every tab to pick it up.
    • Both require a pass with write: true naming that document.

Bug Fixes

  • Pasted HTML ran before anything sanitized it — A pasted <img src=x onerror=…> executed before clean() saw it.
    • Clipboard HTML was parsed into an element owned by the live document, which still loads resources.
    • Untrusted parses now use a document with no browsing context.
    • The URL scheme check moved inside clean(), so a pasted javascript: link is no longer clickable.
  • A long article could not be converted at all — An article that died on the server's engine now converts in milliseconds.
    • Both serializers reallocated the whole accumulated document for every block.
    • blocksToHtml also sent every inline field through a sanitizer that parsed twice.
    • Measured on the real engine: 600 KB went from failing outright to 7 ms.
  • A legacy document's nested content read as empty — Plain text, Markdown and HTML all dropped a legacy block's children.
    • Documents written before nesting moved to parent and content keep their children inside data.
    • Those shapes are now expanded once, in the document model, so every reader sees them.
    • One unreadable block is skipped and reported, rather than costing the caller the whole article.
  • Markdown export lost the fence language and the nesting — Code fences exported unlabelled, and Tab-nested paragraphs exported as code.
    • Four leading spaces after a blank line is an indented code block, so that indent survives only inside a list item.
    • A container rendered only its direct children, so a list inside a toggle lost every level below the first.
    • Lossy conversions now report what changed, instead of reading as "nothing was lost".
  • The caret did not survive a read-only round trip — Entering read-only remembers where the caret stood, and leaving it puts it back.
    • It holds on both the in-place toggle and the save, clear and render path.
    • The restore declines when the user focused something else, or when the block is gone.
    • A collaboration status blip that changes nothing no longer kills a live caret.
  • A link to somewhere on the same page opened a new tab — A bare #fragment reopened the whole document instead of moving inside it.
    • So did the absolute URL that "Copy link to block" hands out.
    • A fragment now scrolls, an identical URL does nothing, and a query-only difference navigates in place.
    • Following a link is reading, so the handler no longer unbinds in read-only.

Maintenance

  • The server runs its own Yjs engine — Nothing it ships carries native code any more.
    • YDotNet and the native build matrix are replaced by a managed C# implementation.
    • Each wave was verified against a shared Yjs oracle rather than against itself.
    • A service account with no home directory now needs no configuration.
  • blok-sync.v2 is on the wire, and deliberately not spoken yet — The service negotiates it, and the client still offers only v1.
    • The operation, acknowledgement and rejection codecs exist on both sides against pinned fixtures.
    • An operation id is settled and journalled before anyone can see the write.
    • Do not build on the v2 frames until the client negotiates them.
  • The documentation site was one step from being indexed as its error state — A crawl recorded the homepage's only <h1> as "Application Error".
    • A component drew with Math.random() during render, so React 19 discarded the server HTML.
    • Docs had also not deployed for three days, blocked by a script demanding impossible release assets.
    • A post-deploy smoke test now asserts what the host actually serves.
  • yarn serve starts the collaboration backend — The dev playground boots the sync service beside it.
    • Multiplayer is exercised locally by default.
    • Pass --no-server to run the playground alone.
  • Gates and dependencies — The .NET solution keeps three test layers plus conformance and package smoke tests.
    • CI requires 80% line and branch coverage on merged production code.
    • It audits NuGet packages, scans for secrets, scans the built image, and analyzes C# with CodeQL.
    • Mutation testing runs on the diff alone, carrying surviving mutants between commits.