Simulators
Word changelog
What changed in each Word simulator release, newest first.
v0.0.2 — 2026-11-05
-
A range’s tracked changes, listed as Word lists them.
getTrackedChanges()on a range answers only the changes in the paragraph the range ends in, clipped to the range, as Word for the web does. A range that ends with a paragraph mark, such asbody.getRange('Whole'), answers none. The body’s and each paragraph’s owngetTrackedChanges()still answer every change. -
Search options take effect, and a refusal answers Word’s code.
searchon a body, paragraph or range honorsmatchCase,matchWholeWord,matchPrefix,matchSuffix,ignorePunctandignoreSpace, whichoffice.jssends packed into one number that the twin used to read as no options at all. A refusal the document makes, such as a location a member does not take, an empty search string or agetReviewedTextversion Word does not know, answers Word’s own code (InvalidArgument,InvalidRequest,NotImplemented) instead ofGeneralException. -
Walk paragraphs with
getNextandgetPrevious.Paragraph.getNext(),getPrevious()and theirOrNullObjectforms answer the neighbouring paragraph, so an add-in that walks a document paragraph by paragraph runs on the twin. Past the last or before the first paragraph they throwItemNotFoundas Word does, or answer the null object, where the twin answeredApiNotFoundbefore. -
Comments answer as Word for the web on a Microsoft 365 work account does. A thread’s
replieslist its first comment as item 0, then each reply, even when there is no reply yet. Deleting that item 0 deletes the whole thread. A comment or reply the twin writes answersuser@example.com, its signed-in user’s address, asauthorEmail, where it answerednull. A resolved thread refuses a reply with Word’sNotAllowedinstead of taking it. -
A tracked change already accepted or rejected stays addressable. Accepting or rejecting it again answers no error, as in Word for the web, and its
type,authorandtextstill load. Before, the twin answeredItemNotFoundorGeneralException. As in Word, a settled change keeps the place it was listed at: rejecting an insertion or accepting a deletion again takes out whatever text now sits there, and itstextreads that text. -
Use an item after loading its collection again. Loading a collection’s
itemsa second time, as an add-in does to refresh a list after an edit, and then calling a method on one of them answers that item as Word does, where the twin answeredApiNotFoundbefore. -
Refusals answer as Word for the web does, and enum arguments take any case.
getRange('After')of the body, or of the document’s last paragraph, answers Office.js’s null object, as Word does, so text inserted there goes nowhere; before, the twin refused it or answered an empty range. A value outside its enum, such as achangeTrackingModeof'Bogus', answersInvalidRequestwith nodebugInfo.errorLocation.Paragraph.insertOoxmlatBeforeorAfteranswersInvalidArgument, andinsertOoxmlof anything but a flatpkg:packageanswersGeneralExceptionand leaves the document as it was, where the twin used to add an empty paragraph. An enum argument spelt in another case, such asgetRange('whole')orchangeTrackingMode = 'trackAll', works as its own spelling does, and a tracked change’sgetRangeanswers the whole change at every location, as Word’s does. -
Take a sub-range of a range, and delete a reply.
Range.getRange()answers the range’sWhole,Start,End,AfterorContentas Word does, and refusesBeforewithInvalidArgument, so an add-in can narrow a search hit before it edits or comments on it.CommentReply.delete()deletes one reply and leaves its thread; deleting a thread’s item 0 deletes the whole thread, as Word for the web does on a work account. Both answeredApiNotFoundbefore. -
getOoxmlpackages hold Word’s default parts. Every package now carries the styles, theme, font-table and web-settings parts Word writes, so a style the document names, such asCommentReference, is defined in the package, and its settings hold Word’s defaults.people.xmlcomes only with comments, as in Word. -
Wildcard search.
searchwithmatchWildcardsmatches Word’s wildcard grammar (?,*,[]and[!],{n,m}and@,<and>, groups in()) as Word for the web does, instead of answeringNotImplemented. A malformed expression answersInvalidArgument, and everysearchrefusal namesBody._search2(or the paragraph’s or range’s) as its location, as Word’s does. -
A comment the twin writes names its author in
people.xml. A comment or reply the signed-in Test User writes now adds a person for that user to the package’speople.xml, with presence providerADand user id0000000000000000, as Word for the web does for its signed-in account. A document seeded without people gains the part. A package names only the persons of authors whose comments or revisions it holds, so a range’sgetOoxmlno longer carries every seeded person. -
A reply anchors after its thread’s last reply. A reply the twin adds to a thread that already has replies now goes after them in the package, as in Word, where it went before them.
-
Count each test’s syncs.
office.stats()resolves to thecontext.sync()round trips, and the actions in them, since the lastoffice.mark()or reset, so a test can hold an operation to a sync budget. The batchesoffice.jssends of its own accord, the release after aWord.runand the ready notice onceOffice.onReadyhas run, aren’t counted, so a test’s count never depends on when they land. The twin serves the same counts atGET /_rystic/inspect?view=officejs. -
Run your suite across your documents.
npx officejs-harness corpus ./fixtures -- npx vitest runruns your suite once per.docxin a folder, each document on its own freshly seeded twin and all at once, and prints a documents × tests matrix of passes, failures and syncs; each failure names the batch that failed and the command that reruns it on that document.office.seedDocx(path)seeds the twin from a.docx, andconnect({ resetTo })makes everyoffice.reset()start from one. -
Text inside a content control around a table row or cell. A seeded document whose content control wraps a whole table row or cell, as repeating-section templates do, now lists that row’s or cell’s paragraphs in
body.paragraphsandbody.text, andsearchfinds their text for an add-in to edit, tracked or not. The twin had skipped them, though the.docxit wrote back kept them.getOoxmlof a paragraph in a table cell now answers that paragraph alone, without the table around it, as Word does. -
Save the document your add-in left.
office.docx()resolves to the twin’s document as a.docx, revisions and comments included, so a test can compare what the add-in made with what a headless OOXML path makes from the same seed. Two runs that make the same edits save byte-identical files, and the twin serves the same file atGET /_rystic/inspect?view=docx. -
Messy documents read as Word for the web reads them. A paragraph’s
getOoxmlis the paragraph alone, without its paragraph properties and outside any block content control that holds it, and a range inside one paragraph is outside the table and control around it too.textreads a soft or no-break hyphen and a carriage return as nothing, a page break as\f, a footnote reference as^Band a no-break space as U+202F, and a Wingdings symbol as its character. A cell a vertical merge continues lists no paragraph inbody.paragraphs. AReplaceof an inline content control’s whole text keeps the new text inside the control, where the twin put it before the control or, untracked, dropped the control. Packages number their bookmarks from 1. Nine messy documents, from PDFs converted to Word to templates full of content controls, are now measured against Word every day. -
getOoxmlwrites a content control around table rows as Word does. Word for the web’sgetOoxmlwrites the rows inside a content control around them, such as a repeating section’s, as they were imported, leaving out text an add-in edited there since, whichparagraphs,searchand tracked changes still read. The twin’sgetOoxmlnow does the same, so a test of an add-in that rebuilds the body fromgetOoxmlloses those edits as it would in Word.office.docx()and the/_rysticdownload still hold every edit. -
A seeded document reads as Word for the web imports it. A simple field (
w:fldSimple) becomes a complex one ingetOoxml, as Word’s import writes it, all but a NUMPAGES with a\* MERGEFORMATswitch. A page number field’s result (PAGE, NUMPAGES, SECTIONPAGES) reads as nothing intext, andsearchneither finds it nor matches across it:Page 1 of 1readsPage of, as in Word. A document with footnotes or endnotes gains Word’s separator notes and their settings, and an endnote reference reads^Bas a footnote’s does. Each package numbers its relationships fromrId1in Word’s order, and holds a hyperlink’s only when it holds the link. A paragraph’s or a range’sgetOoxmlwrites the content control its span ends in as its text alone, as Word’s does; the body’s keeps it. -
Map a range to the body’s paragraphs.
Range.paragraphslists the paragraphs a range touches, as Word does: a range inside a table cell answers that cell’s own paragraph, the onebody.paragraphslists, so an add-in can map a search hit in a table to body paragraph indices. It answeredApiNotFoundbefore. -
A date picker can’t be deleted, as in Word for the web. An edit that would delete any of a date picker content control’s text, such as
body.clear()or aninsertText(…, 'Replace')over it, now answersNotAllowedand leaves the document as it was, so an add-in that clears a template holding one fails on the twin as it does in Word for the web. Text inserted at the start of a range inside one answersNotAllowedtoo, and deleting a range that only touches an inline one’s edge answersGeneralException. -
people.xmlnames the author of a tracked change who comments. AgetOoxmlpackage now names, inpeople.xml, the author of a revision in it when that author comments anywhere in the document, as Word for the web does, so a paragraph holding only a commenter’s redline carries their person. After Test User’s first comment, its own tracked edits name it too. Persons are listed in the order their authors first appear in the package, and a seeded person whose author never comments is no longer named. -
See what a sync count is made of.
office.batches()resolves to the batches the twin counted since the lastoffice.mark()or reset, each with every action named by its type and the object it acts on, such asQuery Body._Search2(), so a count that comes out higher than expected names the batch that made it so. The twin keeps its latest 128 counted batches since its last reset atGET /_rystic/inspect?view=officejs-batches, and a corpus run’s batch log holds the batches of each test. -
Replacewrites unformatted text, as Word for the web does.insertText(…, 'Replace')now gives its text no formatting of its own, tracked or not, where the twin had copied the formatting of the text it replaced; replacing a bold phrase writes the new phrase plain. An edit at the end of a paragraph gives its paragraph mark the formatting of the paragraph’s last run, as Word does, sogetOoxmland the saved.docxmatch Word’s. -
Text inserted in a cell control inside a row control is refused, as Word does. Inserting text inside a content control around a table cell, when a content control around its row holds that cell, as in a repeating section’s items, now answers
NotAllowedand leaves the document as it was, tracked or not, whether through the paragraph, a range in it or the control, as in Word for the web. The twin had made the edit. A control around the cell alone, or around the row alone, still takes it. -
getOoxmlof a paragraph inside a content control around table rows answers as Word does. Word for the web writes such a paragraph’s package, or a range’s inside one, from the paragraph as it was imported, at the same offsets, and refuses it withGeneralExceptiononce the paragraph’s text runs past that copy, as after an add-in appends to it, or when the paragraph was inserted there. The twin now does the same, so a test of an add-in that reads a repeating section item’sgetOoxmlafter editing it meets the error it would meet in Word. -
A collection loaded again in one run lists what it first held. As in Word,
body.paragraphs, a range’s paragraphs, a search’s hits, comments and a thread’s replies loaded again in the sameWord.runlist the items Word resolved when the run first reached them, each read as it is now: a paragraph deleted since stays with empty text, one inserted since is not listed, and the deleted paragraph’s own object throwsItemNotFound. A tracked change collection still lists the changes as they are. The twin listed the document as it was at every load before. -
getOoxmlof a range over table cells writes the package Word does. A range that starts or ends inside a table cell now answers a package holding only the rows it reaches, the row it starts in from the cell it starts in. A range that ends inside a cell writes what it covers of that row as a table of its own, then the paragraph it ends in as a body paragraph after the tables, as Word for the web does. The twin had written the whole table, with the cells outside the range left blank. -
getOoxmlwrites a date picker as Word for the web does once text goes in at its start. After your add-in inserts text at an inline date picker’s start from outside it, the body’s and the paragraph’sgetOoxmlwrite the picker in two, an empty control without its date and then one with a new id holding that text and the date, as Word for the web does, so an add-in that reads date pickers back from the package sees on the twin what it sees in Word. -
Deleting or replacing part of a paragraph’s text inside a row control is refused, as Word does. With deletions untracked,
Range.deleteof part of a paragraph’s text inside a content control around table rows, such as a repeating section’s, andinsertTextwithReplaceover part of it now answerGeneralExceptionand leave the document as it was, as in Word for the web; the twin had made the edit. A delete from part of one paragraph into the next there takes only the text of the paragraphs it holds whole. A paragraph’s whole text, inserted text,Paragraph.delete,insertOoxmland tracked edits go through. -
Deleting the document’s last paragraph empties it, as Word does.
Paragraph.delete()on the document’s last paragraph, or on a table cell’s, now takes only its text and leaves the paragraph, empty, as in Word for the web, sobody.paragraphsstill lists it and the document still ends in a paragraph. Tracked, the paragraph’s text is marked deleted and its paragraph mark is not. The twin had removed the paragraph, and after a table it had left the document ending in the table. -
Deleting a content control’s only paragraph removes the control, as Word does.
Paragraph.delete()on the only paragraph of a block content control now takes the control with it, as in Word for the web, so the body’sgetOoxmland the saved.docxno longer hold an empty control there; a paragraph that ends a table cell stays, empty, in the control’s place. Tracked,getOoxmlwrites the control’s edges as deleted,w:customXmlDelRangeStartandw:customXmlDelRangeEndpairs, as Word does, and so does any tracked delete that takes a block control whole, such asbody.clear(). The twin had emptied the paragraph and kept the control. -
A deleted comment in a collection loaded again refuses its content, as in Word. A comment or reply your add-in deletes stays in a comment or reply collection the same
Word.runreached before, with itsid, author, date andresolvedstate, but reading itscontent, or loading the collection’s items with every property, now throwsGeneralException, and the deleted comment’s own object throwsItemNotFound, as in Word for the web. The twin had read the deleted comment’s content. -
A group content control is locked, as in Word for the web. A document holding a group content control now imports it locked: an edit to text in its paragraphs, such as
insertTextordeletethrough a paragraph or a range, or a delete across the group’s edge, answersNotAllowedand leaves the document as it was, tracked or not. The twin had made the edit. A rich-text control inside the group still takes edits, andbody.clear(), aReplaceof the whole body and a delete of a range around the group take it with them, its paragraphs too. -
A tracked
Replaceof the body writes its text untracked, as Word does. With tracking on,body.insertText(…, 'Replace')now marks the old text deleted and writes the new text before it with no insertion, sogetTrackedChangeslists only the deletions, as in Word for the web. The twin had tracked the new text as an insertion. AReplacethrough a paragraph or a range still tracks it. -
A range inside a deleted paragraph goes with it, as in Word. Once an untracked
delete()removes a paragraph, a range asearchorgetRangemade inside it in an earliercontext.sync()that only read throwsItemNotFoundfor the rest of the run, as the paragraph does. A range made in the same sync as the delete, or byexpandToorinsertText, still reads empty at the start of the next paragraph, and a range that runs into the deleted paragraph now ends where the text before it ends. The twin had read every such range where the paragraph collapsed. -
Deleting a cell’s only paragraph in a one-row table takes its column, as Word does. In a one-row table, or for a cell merged down every row, an untracked
Paragraph.delete()of the cell’s only paragraph, orRange.delete()of the whole cell, now removes the cell and its column, and the table with its last column, as in Word for the web; the twin had emptied the paragraph. Deleting a paragraph that holds a comment’s anchor now removes the comment with it; the twin had failed the request. -
getOoxmlwrites a package’s last paragraph without its properties, and text at a paragraph’s end takes its formatting as Word does. EverygetOoxmlpackage now writes its last paragraph without its alignment, style, numbering or mark formatting, as in Word for the web, which still holds them: the body’s leaves out the document’s last paragraph’s, and a range’s those of the paragraph it ends in. Text inserted untracked at a paragraph’s or the body’sEndtakes the formatting of a paragraph mark that has its own; with tracking off, an edit at a paragraph’s end gives its mark the last run’s formatting;insertParagraphgives the new paragraph’s mark no formatting of its own, takes it off the paragraph’s own mark withAfterthough not withBefore, and gives the new text the formatting of the run beside it; and text typed into the document’s emptied last paragraph takes the formatting of the run deleted. The body’sinsertTextatStartorEndnow writes untracked text with tracking on, as itsReplacedoes. The twin had written every paragraph’s properties and tracked that text. -
An
insertOoxmlputs a content control around table rows back asgetOoxmlwrites it, as Word does. In Word for the web, aninsertOoxmlanywhere in the document, at any location, first gives each content control around table rows, such as a repeating section’s, the rows itsgetOoxmlwrites: what an add-in changed inside one since goes, text inserted, deleted or replaced, a paragraph emptied or inserted, and tracked edits with their tracked changes. TheinsertOoxml’s own edit stays, inside the control too, and the body’sgetOoxmlwrites it from then on; into a paragraph there, it lands at the same offsets in the textgetOoxmlwrites, and answersGeneralExceptionwhen it ends past that text. The twin now does the same; it had kept those edits, and written the control as first imported. -
A delete or Replace across a table cell’s edge is refused, as Word does.
Range.delete()andinsertText(…, 'Replace')of a range that runs from one table cell into another, or into or out of a table, now answerGeneralException, tracked or not, and leave the document as it was, as in Word for the web, unless the range runs from the table’s first cell, or before it, into its last cell or past it; the twin had deleted the text in each cell. Untracked, such a delete that holds the table’s start or end now takes the cells it holds whole, and the table with its last; aReplacedeletes the same way before it writes its text; and a tracked delete no longer marks a cell’s ending paragraph mark deleted. -
Accepting the delete of a content control’s only paragraph leaves the control inline, as Word does. Accepting a tracked
Paragraph.delete()of the only paragraph of a block content control, one change oracceptAll(), now joins that paragraph to the next and leaves the control there, empty and inline at the next paragraph’s start, as in Word for the web, sobody.paragraphslists one paragraph fewer and the body’sgetOoxmlwrites the control inside the next paragraph, its edges still marked deleted. Accepting or rejecting any change that takes the mark of a control’s only paragraph, such as a seed’s deletion or insertion, does the same. Accepting the deleted mark of the paragraph just before such a control joins the two the same way, the control inline around its text. A control that ends a table cell, or that a table follows, keeps its paragraph, empty. The twin had kept the control as a block holding an empty paragraph. -
Just Mine tracks your edits as For Everyone does, but for
insertOoxml. UnderchangeTrackingMode = TrackMineOnly, the text a paragraph’s or a range’sinsertTextputs in, and the mark of the paragraphbody.insertParagraphadds (not its text), are now tracked, as in Word for the web; the twin had tracked only deletions.insertOoxmlnow tracks nothing in that mode: aReplacedeletes the old text untracked and the package keeps its own revisions, as with tracking off. -
Deleting a paragraph’s whole range removes the paragraph, as Word does. With tracking off,
delete()on a range that holds a paragraph whole, such asparagraph.getRange('Whole'), now removes the paragraph as its owndelete()does, and a range over several paragraphs removes each one it holds whole, where the twin had emptied them and kept their paragraph marks. A range asearchorgetRangemade inside such a paragraph in an earliercontext.sync()now throwsItemNotFound. Tracked, the mark that ends the document or a table cell is no longer marked deleted, and a range deleted through the document’s last mark throwsItemNotFoundafterwards, as in Word for the web. -
A range delete around a block content control keeps the paragraphs either side, as Word does. With tracking off,
delete()on a range from inside the paragraph before a block content control to inside the paragraph after it now takes the control and keeps both paragraph marks, as in Word for the web, so the text left either side stays in two paragraphs. This holds for a control of any kind, a group content control included, for nested controls and for several controls in a row. The twin had joined the two paragraphs into one. -
A range keeps its offsets when its text shrinks, as in Word. With tracking off, once an untracked
deleteshortens a paragraph’s text, a range asearchorgetRangemade there in an earliercontext.sync()that only read reads no further than the text’s end, where the twin had read the paragraph mark too, and a range over text the delete emptied answersisEmptyfalse while it reads empty. Text inserted at such a range past the text lands at the text’s end, not at the start of the next paragraph, and an end past the mark, asgetRange('Whole')has, stays past it as the text grows or shrinks. -
An
insertOoxmlReplace across paragraphs joins them, across table cells too, as Word does. With tracking off,insertOoxml(…, 'Replace')over a range that runs across paragraph marks now takes everything between, a block content control included, and joins the text before and after the range around the package, as in Word for the web. Across table cells it joins the cells and rows it crosses into the first cell it reaches, a shorter row skipping the rest of the grid withgridAfter, and over exactly one table it takes the table and joins the package to the paragraph after. UnderTrackMineOnlyit joins the same way, and underTrackAllit now changes nothing, as Word’s reads show. The twin had kept every paragraph and cell, and tracked the edit. -
A range moves with the text unless a sync that only read made it, as in Word. A range a
searchorgetRangemade in acontext.sync()that only read still keeps its offsets in its paragraph through later edits, but every other range now follows the text: one made in a sync that wrote anything, such as an edit anywhere or achangeTrackingModeset, one made in the edit’s own sync, oneinsertTextanswered, and one aninsertTextordeletewent through. Replacing every hit of a search made in a sync that wrote now lands each replacement on its own hit, and such a range inside a paragraph a later untrackeddelete()removes reads empty at the start of the next paragraph rather than throwingItemNotFound.insertOoxmlof a one-paragraph package moves no range but its own. The twin had kept every range at its offsets, but a tracked insertion’s own search hits. -
A
Replaceacross paragraphs deletes asdelete()does, as Word does. With tracking off,insertText(…, 'Replace')over a range that runs across a paragraph mark now deletes asdelete()does, taking any block content control between and keeping the paragraphs either side apart, then writes its text where the range collapsed; a paragraph mark the range ends just past stays. The twin had kept the controls and an empty paragraph for each paragraph between. With tracking on, the new text now goes after the deleted text, in the paragraph the range ends in, as in Word for the web, where the twin had written it at the range’s start. A delete orReplaceacross a paragraph mark also gives the marks it leaves the formatting of the text it took before them. -
Spaces side by side read as Word for the web reads them under tracking. Once an edit with tracking on changes a paragraph’s text,
textreads each run of spaces in it plain and no-break (U+00A0) in turn, a no-break space first at the paragraph’s start:insertText,insertParagraph, a trackeddeleteof your own insertion, and an accept or reject that takes text out. Before, the twin read plain spaces where a tracked insertion left two side by side, and read the second of two an accept or reject left as a no-break space with tracking off too.getOoxmlandsearchkeep plain spaces. -
A paragraph a package of several paragraphs splits keeps its ranges in its last part, as in Word. After
insertOoxmlof a package of several paragraphs, or one holding a table, into a paragraph, theParagraphobject, and every range in it but the one the insert went through, reads the last part, the one holding the paragraph’s mark, at the same offsets, so a search hit after the insertion point reads the text now at its offsets there. A tracked change listed before reads the text at its offsets there too, and settles by them. The twin had kept such ranges in the first part, and settled such changes as listed. -
An
insertOoxmlreads every paragraph’s spaces as Word for the web does. Once aninsertOoxmlgoes through, tracking on or off,textreads every paragraph in the document afresh: a space that starts a paragraph, ends it or is followed by another reads as a no-break space (U+00A0), every other as a plain space. Before, the twin left each space reading as it did.getOoxmlandsearchkeep plain spaces. -
A tracked
insertOoxmlReplace across a paragraph mark answers the range Word does. UnderTrackAll,insertOoxml(…, 'Replace')over a range across a paragraph mark still changes nothing the reads show, and now answers a range from the range’s start to the start of its last paragraph, or over exactly one table to its end, as in Word for the web; the range it was called on reads the same. Word’s owngetOoxmlwrites a half-applied edit after such a Replace, which its reads show only once that package is read; the twin follows the reads and writes the document as it was. -
A paragraph break inside an inline content control makes it a block control.
insertTextwith a newline, orinsertOoxmlof several paragraphs, through a range inside an inline rich-text content control now turns the control into a block control around the paragraphs it then spans and splits the paragraph at the control’s edges, as Word for the web does;contentControlslists it once with those paragraphs’ text, and a locked control refuses the split withNotAllowed. Before, the twin kept the control inline, in two halves with one id. -
A package of several paragraphs at a
Wholerange’sEndreads as in Word.insertOoxmlof a package of two or more paragraphs at theEndof a paragraph’sgetRange('Whole'), whose end is past the paragraph mark, splits the paragraph and leaves the range ending at the last part’s text, short of the mark, where the twin had read the mark too. A range that kept its offsets, one acontext.sync()that only read made, then reads one paragraph mark more after the first part,…a\r\rbfor a package ofaandb, as Word for the web does, though the document holds no paragraph there, until an edit goes through it, and so do theWholeandContentrangesgetRangetakes from it; a range that moves with the text reads…a\rb. A package that starts with a table leaves the range over the package’s content. -
body.clear()and the body’sReplacekeep the last paragraph, as in Word. With tracking off,body.clear()andbody.insertText(…, 'Replace')now remove every paragraph but the document’s last, which stays and takes the new text: abody.paragraphsreached before reads the others empty and the last with the new text, a range asearchorgetRangemade in a removed paragraph in an earliercontext.sync()that only read throwsItemNotFound, the body’sgetRange('Start'),ContentandWholeincluded, and any other range collapses to the start of the paragraph that stays, where after aReplaceone from a removed paragraph reads the new text, answersisEmptytrue and takes an insert after that text. The twin had kept the first paragraph, read every range in the others as empty and taken text inserted at them silently. -
acceptAllandrejectAllstop at a joined paragraph, as Word does. They now settle a collection’s changes in the order it lists them, and once one takes a paragraph mark out, joining that paragraph to the next, a change listed in the next paragraph has lost its paragraph: a text change settles nothing and stays listed, and a change holding that paragraph’s mark answersGeneralException, which ends the call with it and every later change still tracked, as in Word for the web; a singleacceptorrejectof such a change answersItemNotFound. SorejectAllover two adjacent inserted paragraph marks, such as twoinsertParagraphcalls at the body’sEndor the pair aRange.insertParagraphsplit leaves, rejects the first and refuses the second, andacceptAllover two adjacent deleted paragraphs accepts the first and refuses the second. Two text changes in one paragraph settle in that order too, the second keeping the end it was listed with, sorejectAllover an insertion and a laterReplacein one paragraph now removes the replaced text and what follows it, as Word does. Accepting one author’s deletion of a paragraph mark once another author’s deletion of that paragraph’s text was accepted now answersGeneralExceptionand drops the mark’s deletion, the paragraph staying in place, as in Word. The twin had settled every change, last first, and joined the paragraphs. -
An
insertOoxmlReplace into or out of a block content control joins and leaves the control inline, as Word does. With tracking off,insertOoxml(…, 'Replace')over a range that starts or ends inside a rich-text block content control’s paragraph now joins the paragraphs around the package as in Word for the web: a control left holding only that paragraph goes inline around what is left of its text, empty when nothing is, or goes when the range held it from its start, while a control that keeps other paragraphs stays a block and keeps the text after the range apart. The package lands inside the control the range began in, past any spaces that ended the control’s text, which leave it, as they do for an inline control; a package that would land inside a plain-text control is refused withNotAllowed. The twin had kept the control’s paragraph apart. -
A range that moves follows its text into the rest of a paragraph
Range.insertParagraphsplits. As in Word for the web, wheninsertParagraphthrough a range splits a paragraph, a range made in acontext.sync()that wrote, or byexpandTo, now follows its text: a hit past the split reads its text in the rest, a hit across it reads across the new paragraph, and a span from it into the next paragraph starts in the rest. A range a sync that only read made keeps its offsets in the first half, as before. The twin had kept every range but the one the insert went through in the first half. -
A document read in reads its spaces as Word for the web does. Before any edit,
textreads a space that starts a paragraph, ends it or is followed by another as a no-break space (U+00A0), every other plain, as aninsertOoxmllater reads them; before, the twin read plain spaces until aninsertOoxml. Every other character keeps spaces apart, a field whose result reads as nothing or a drawing too, and so does each edge of a content control, an empty one too, or a hyperlink: a space at a control’s edge, or the first or last of a paragraph a block, row, cell or table control holds, reads plain, after aninsertOoxmlas well.searchmatches a no-break space, in the search text or the document, with a plain space. -
An edit from inside a block content control’s last paragraph through its mark ends the control where it started. As in Word for the web, an untracked
Range.deletefrom inside the paragraph’s text through its mark, or of that mark alone, and aRange.insertText(…, 'Replace')whose range ends just past the mark, leave a control holding that one paragraph inline around the text before the edit, plus the text theReplacewrote in place of what it took, so text inserted at the range afterwards lands after the control; a control holding more paragraphs stays a block and that text starts a new paragraph after it. UnderTrackAll, aReplacefrom before the control that writes after the deleted text at the control’s end does the same and marks the control’s edges deleted. The twin had kept the control a block holding the inserted text. -
A tracked
insertOoxmlReplace over a paragraph’s Whole range, or from a paragraph’s start across its mark, now goes through as Word does. UnderTrackAll,insertOoxml(…, 'Replace')over a range that ends just past a paragraph mark deletes short of that mark, which stays, and one from a paragraph’s start across marks lands as a tracked edit, the package at the range’s start and the text to the last paragraph’s start marked deleted; only a range from inside a paragraph’s text across a mark the document had, or over the body’s or a table’s whole text, still changes nothing. A tracked package of two or more paragraphs now tracks the marks it adds and its last paragraph’s text, never the text before them, so a later tracked deletion over that text is a plain deletion, and aninsertOoxmlReplace’s deletion takes over a deletion already in its range. The twin had tracked every paragraph’s text, removed the first paragraph’s under a later Replace, merged the paragraphs and refused the paragraph after. -
An
insertOoxmlReplaceacross paragraph marks keeps the last paragraph’s object, as in Word. After an untrackedinsertOoxml(…, 'Replace')over a range across one or more paragraph marks, theParagraphobject of the paragraph whose mark ends the range reads the joined paragraph, and every range in it, a search hit from a sync that only read or one that wrote, keeps its offsets in the joined text; the paragraphs the range starts in and crosses are gone, so their objects, every range in them, every range running into them and every tracked change listed in them answerItemNotFound. Across table cells the paragraph of the cell the range ends in keeps its object, and over a whole table the paragraph after does. The twin had kept the first paragraph’s object and shifted the last’s ranges into it. -
Text inserted right after a tab is a tab run in
getOoxml. As in Word for the web, text your add-in inserts right after a tab under tracking is written bygetOoxml, the body’s, the paragraph’s and the insertion’s own, as onew:tabwhatever it holds, a space, two, a letter, or a letter and a space, each stretch between tabs it holds as one, throughBeforeorStartbut not aReplace, and still once the insertion is accepted;textandsearchread the text and the twin’s saved.docxkeeps it. Before, the twin wrote the text. -
A package that starts with a table adds no empty paragraph at a paragraph’s start, as in Word.
insertOoxmlof a package whose first block is a table, at a paragraph’s start, through the paragraph, the body or a range there, at any location, tracked or not, puts the table right before the paragraph, which keeps its object, its mark and every range in it, and a table already right before it, of the same layout and table style, takes the package table’s rows, each keeping in aw:tblPrExthe alignment, borders, shading, cell spacing and margins its own table differed in, as Word for the web does. The twin had split off an empty paragraph before the table. A range a table-first package went through at itsStartnow starts at the table, there or inside a paragraph’s text,T1\rbbbbfor a hit onbbbb, where the twin had started it at the paragraph mark before the table, and a collapsed range it went through at itsEndgrows over it the same way. A range that ends where a package ending in a table ends now ends past the table’s last cell mark, so text inserted at itsEndlands in that cell, where the twin wrote it at the start of the paragraph after; and tables side by side in a package join as one, where the twin kept them apart. -
A paragraph’s
Afterrange sits at the next paragraph’s start, and aWholerange’sEndat the text’s end. As in Word for the web, text your add-in inserts through a paragraph’sgetRange('After'), or theAfterof itsWholerange, lands at the start of the paragraph after it at every location,Replaceincluded, in a table’s first cell or the next cell too, and the range stays there when a paragraph goes in between. ThroughgetRange('Whole').getRange('End')it lands at the paragraph’s text end, as throughgetRange('End'), taking the paragraph mark’s formatting atStart. TheAfterof the last paragraph’s or the body’sWholerange is a null object, and a spanexpandToends at the next paragraph’s start holds the paragraph mark, so text inserted at itsEndlands before the mark. Before, the twin held both ranges just past the mark and wrote at the text’s end. -
An
insertOoxmlputs back each range your add-in has not reached since it moved, as in Word. Word settles a range’s place when your add-in makes it, reads it, calls a member on it, passes it as an argument, takes it withgetFirstor edits through it. AninsertOoxmlanywhere, tracked or not, now puts every other range that moves with the text back where it sat then, so a hit after textinsertTextadded, or after text a delete, an accept or a reject took, reads its old offsets again. A range aParagraph.deletecollapsed to the next paragraph’s start throwsItemNotFoundonce aninsertOoxmllands in that paragraph, and one that ran out of, into or across the deleted paragraph, or that the document’s last paragraph held, throws it after anyinsertOoxml, until your add-in reaches it again. A package inserted through such a collapsed range atStartorEndis now held by the range. The twin had kept every range where the edits moved it. -
A change listed before a split settles by its offsets in the part holding the mark, as in Word. After
insertOoxmlof a package of several paragraphs splits a paragraph, accepting or rejecting a tracked change listed before settles what its offsets now hold there. Accepting an insertion, rejecting a deletion or settling a formatting change settles each change of its kind whose text lies wholly inside them, and rejecting an insertion or accepting a deletion takes out the text there, short of where its mark was, which the change’stextalso stops at. A paragraph mark inside them has its revision of the change’s kind settled too, whoever made it, and one of another kind left alone; where that settle would take the mark out, the mark stays with the revision dropped, unless the change held its mark at that very offset, where the paragraph joins the next. Rejecting an insertion, or accepting a deletion, that held its mark elsewhere also answersGeneralException. A change settled before, split or not, likewise reads and takes out its text short of where its mark was, where the twin had reached one character past it. The twin had settled none of the text such a change’s offsets held and left the mark as it was. -
A tracked
insertOoxmlnow tracks the tables in its package. As in Word for the web, underTrackAlleach row of a package’s table and each cell’s paragraph marks and text are marked inserted, each cell paragraph listing as anAddedchange, a package that starts with a table leaves the mark it splits off untracked, and the insertions and deletions a package itself carries give way to the insert’s own tracking; the twin had put the tables in untracked and kept the package’s revisions. -
A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.
-
The call log shows what each call asked for and whether it succeeded. Each
GET /_rystic/callsentry now carriesargs, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), andok, false for an answer that is not 2xx. An answer carrying anErrorobject at HTTP 200 is not ok either, and its code is the entry’serror. A path the simulator does not serve is markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it. Tracked changes, comments, the document view, the saved.docx, the corpus runner, content controls, tables, ranges, text edits, spaces and OOXML each have their own page underdocs/features/, and every example that talks to the simulator over HTTP runs as written against a fresh simulator. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
-
The Scenarios table is easier to scan. Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.
-
Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.
-
Limitations is shorter to read. It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind “Show all limitations”.
-
The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.
-
The docs read more plainly, and insertOoxml has two new pages. Sentences are shorter, and each behavior that differs from Word for the web is marked “Differs from Word for the web” where it’s described. Rules with several outcomes, such as what a delete leaves of each range, are tables. How insertOoxml moves ranges, and what its Replace does across paragraphs, move to new pages that each open with an example. Seeding state and Determinism and reset link the shared pages for the seed, clock and reset rules every simulator follows, and Auth, Seeding state and Determinism and reset keep only what is particular to Word.
v0.0.1 — 2026-10-14
-
A local Word host for your Office.js add-in’s tests. The first release of the Word twin: your add-in loads Microsoft’s
office.jsfrom its CDN, unchanged, and the twin plays Word for the web to it, answering eachcontext.sync()as Word does across 86 members of Word’s API — the body, paragraphs and ranges, search and edits, change tracking, tracked changes, reviewed text, comments and OOXML — from a model validated against live Word for the web. Your suites run with no Microsoft 365 account, no sideloading and a fresh document for every test. -
The harness: Jest, Vitest and Playwright suites against the twin. The twin serves
@rystic/officejs-harness, MIT-licensed, at/officejs/harness.tgz, sonpm install --save-dev http://127.0.0.1:8080/officejs/harness.tgzinstalls the version that matches it. In Node,connect()loadsoffice.jsinto a jsdom window whose host is the twin,install()putsOffice,OfficeExtensionandWordon the global object, andreset()starts a fresh document. In a browser, the twin’s host page/officejs/host?addin=<url>frames your task pane:openAddin()routes it inside a Playwright context with no certificate, andofficejs-harness servefronts it over https on localhost. -
Read, search and edit the document as
Word.runsees it.context.document.body, its paragraphs and their ranges answertext,getFirst,getLast,itemsandgetRange;searchon a body, paragraph or range takesoffice.js’s options (matchCase,matchWholeWord,matchPrefix,matchSuffix,matchWildcards,ignorePunct,ignoreSpace); andinsertTextat each location Word takes,Body.insertParagraph,Body.clearanddeleteedit it. The handshake declares WordApi 1.6 and WordApiSearchFlags 1.1, soisSetSupportedanswers for what the twin serves, and a member it does not serve answers Word’sApiNotFound. A reach that finds nothing raises Word’sItemNotFoundfromcontext.sync(). -
Tracked changes, reviewed text and comment threads. Setting
changeTrackingModetoTrackAllorTrackMineOnlymakes every edit a tracked insertion or deletion;getTrackedChangeson a body, paragraph or range lists them withtype,author,dateandtext, andaccept,reject,acceptAllandrejectAllresolve them.getReviewedText('Current')and('Original')read the text with and without the pending changes.Range.insertCommentanchors a comment,getCommentslists them,replyadds to a thread, settingresolvedresolves it anddeleteremoves it.getOoxmlanswers the flat-OPC package Word answers, with the redline asw:insandw:del, andinsertOoxmlplaces one. -
Contracts to start from, and your own.
--scenario word_contract_redlinedopens a mutual NDA mid-negotiation, with Alice’s and Bob’s tracked changes, Alice’s comment and Bob’s reply, and a resolved comment;word_contract_cleanopens the same NDA with every change accepted, to redline from scratch;SCENARIOS.mdsays what each is for.POST /_rystic/seedopens your own contract from a base64.docxor its OOXML parts, keeping its revisions, comments, authors and dates, andPOST /_rystic/resetopens a blank document. The clock never moves by wall time, so two identical runs give byte-identical OOXML./_rystic/uishows the open document as Word’s review view does, redrawn as eachcontext.sync()lands. -
Measured against live Word for the web. The full-suite round of 2026-10-14 (
20261014T014904Z) matched 74 of 86 probes against Word for the web, none inconclusive.HOWTO.mdstates the differences this release ships with: a new comment’s or reply’sidis a fixed number and itscreationDatethe twin’s clock;getTrackedChangesongetRange('Whole')lists an insertion Word leaves out;Paragraph.insertOoxmltakes a package Word refuses; andsearchwith a plain options object, whichoffice.jsnever sends, finds hits where Word refuses it.