mawy 1.8.0 copy "mawy: ^1.8.0" to clipboard
mawy: ^1.8.0 copied to clipboard

A Markdown editor and viewer that draw the document rather than a string of HTML — their own CommonMark and GFM parser, typography the reader controls.

Changelog #

This package's history. Mawy keeps a separate changelog for each language it ships, beside that package's own manifest, because the two version independently.

vNext #

1.8.0 - 2026-09-24 #

Added #

  • The typographer, which draws the marks a keyboard has no key for. A document written for print says "a", -- and (c) because a keyboard has nothing else to say them with, and every one of them came out as the characters that stood in for the mark. MawyParseOptions gained typographer, off by default, and with it a quotation mark is turned round the way it faces and --, ---, ..., (c), (r), (tm) and +- are drawn as the marks they stand in for. Which way a quotation mark faces is decided by what sits either side of it, across the markup between one mark and its other half, so it's, 'tis a pity and dogs' bones come out as an apostrophe, an opening mark and a closing one, and a mark nothing closes is left as it was typed — which is what keeps 5" 6" saying feet and inches. The substitutions are markdown-it's, character for character, because these are conventions rather than decisions and a document written against that reader has to come out of this one the same way. A character the document escaped is left as it was escaped — \"a\" keeps its quotation marks and a\-\-b its two hyphens, because an escape is how Markdown says “this character, literally” and that is an instruction rather than a typo. A character reference is not an escape and is not affected. Nothing a machine reads is touched: a code span, a code block, a link's target and title and a picture's description are left as written, and so is a bare address whether or not it is drawn as a link, since http://a.co/a--b is one address and http://a.co/a–b is another. It is off by default because it rewrites characters the author typed. It is the React package's reading, and the parity check compares the two.

  • A bare ftp://, matrix: or //host/path can be read as the address it is. A bare address became a link only in the three shapes GFM reads — http, https, www. and an e-mail address — so every other scheme the viewer would happily follow was words. MawyParseOptions gained autolinkSchemes, off by default, and with it a run that looks like an address carrying a scheme is read as one. Which schemes is not a second list to keep in step: the run only has to look the part, and the scheme allowlist then refuses everything the viewer will not follow, so javascript:alert(1) and data:text/html,x stay words with the option on exactly as they do with it off. A colon in a sentence is not a scheme either — TODO:fix, note: see below and ratio 3:2 are refused because nothing on the allowlist is called that, and C:\Users because a scheme is at least two characters long. An address starting // leaves its scheme to the page and needs a dot in its host, which is what keeps //server/share and a stray //comment from becoming links. Off by default because GFM stops at the web addresses, and a run that is a link here and words on GitHub is a document that means two things. It is the React package's reading, and the parity check compares the two.

  • A quotation can be drawn with the marks its language uses. The typographer drew “a” and nothing else, which is English and Korean and is not German or French. MawyParseOptions gained typographerQuotes, four marks of which every one keeps the English default when it is left out, so MawyQuotes(doubleOpen: '„', doubleClose: '“') is the whole of what a German document needs and all four is how a quotation inside a quotation gets its own. The apostrophe is deliberately not one of the four: dogs’ bones wants ’ whatever the quotations are set to, and a document that set its single marks to ‚‘ would otherwise write dogs‘ bones. MawyQuotes is the new type. It is the React package's reading, and the parity check compares the two.

  • MawyDocument, for a document that is one item of a list. MawyViewer is a surface with a scroll view of its own, so a document drawn inside a message list or a feed had no height to be given and threw during layout — which left an application boxing every message at a height it had guessed, or not drawing documents in a list at all. MawyDocument is the same parser, the same renderer and the same palette, drawn as a column exactly as tall as the document, with the scrolling left to whatever it sits in. It takes the settings that mean anything without a surface — typography, colorScheme, tokens, links, images, directives, highlight, imageBuilder, resolveUrl, onLinkTap, padding, locale and strings — and leaves out the toolbar, the find bar and the outline, each of which acts on a scroll position it has not got. A selection is a SelectableRegion the application places where it wants one to reach, and a link is still followed under it. A note's number takes the reader down to the note and back by moving whatever does the scroll, so a document inside a list scrolls the list. Nothing is built lazily, because a column is as tall as its children; a document long enough for that to show is one that wants a viewer of its own. The name is the React package's, where the same meaning is a document drawn with no viewer around it on a server.

  • imageUrls, the pictures a document points at. An application that stores the pictures it writes into a document had no way to tell which of those files a document still needs, and the answer is not a callback when a picture is deleted: an undo puts it back, a cut picture is pasted somewhere else, and the same address copied into a second document is one file in two places, so a file deleted at that moment is a picture missing from a screen later. imageUrls(parseMarkdown(saved)) lists every address a saved document draws a picture from, each once, as the document writes it, and comparing that with what was stored across every saved document finds the files nothing points at. It reads generously, because a file kept costs its bytes and a file deleted costs a picture: a picture inside a link, a table cell, a directive's label or a footnote something refers to, and the src of an <img> in raw HTML although this package draws none, one inside a comment included. It is the React package's function in Dart, and the parity check compares the two.

1.7.0 - 2026-09-17 #

Added #

  • MawyCodeGroup, the one drawing this package ships for a directive. The parser reads the shape and stops there, so a name is the application's to choose — but one directive turns up in every set of documentation and the drawing for it is the same everywhere: one piece of code written several ways, with a tab per language. Register it under whatever name your documents use, code-group or lang or tabs, and a group of blocks is drawn as tabs named by the language each fence was written with. {tabs=Browser,Flutter} names them instead where a fence's language is not the word a reader should see, and a group holding one block takes its name from the title the line wrote. One tab is in the traversal and the arrows move between them, Home and End to the ends, which is MawyRoving — the same focus model the toolbars use. Only the chosen block is built, so what the screen holds is what a reader can see. It is the React package's component in Dart, and the two name their tabs the same way.

  • A directive's head can be written the way every tool that shipped one writes it. The proposal's spelling is :::name[label]{key=value}, with the name against the colons and nothing after the head; VitePress, Docusaurus and Python-Markdown's admonitions all write ::: name Some title instead, and a document written that way came out as three paragraphs with its blocks loose between them. Both are read now, and the title is the label said another way, so a component is handed the same label either way. Only where the line wrote no [label] of its own, since two labels on one line is a line that means two things. The cost, written down where the rule is: a line of prose opening :: like this is a directive rather than a paragraph, which is the trade every reader of this syntax has already made, and a directive nobody registered is still drawn as the characters it was written with. It is the React package's reading, and the parity check compares the two.

  • The viewer's toolbar has a button that shows the document as the Markdown it was written in. A drawn document answers every question about a document except how it was written, and a reader who wanted that had to be handed the file. raw is the same pane with the characters of the source in it rather than what they mean, and everything the toolbar says about type — the typeface, the size, the line height, the letter spacing, the measure and the palette — still saying it. outline and find read a drawn document, so both are off while it is on; copy is not, because it takes the Markdown either way. MawyViewerToolbarItem gained raw, and kMawyViewerToolbar has it beside copy.

  • A run fenced by --- at the very top of a document is the metadata it looks like. It came out as a rule with a heading underlined by another, which is what that notation is to Markdown and what no reader of it means — every static site generator carries a title, a date and whatever else beside a document that way, and a reader is shown none of it. It is read as metadata now and kept out of what the document says. The fence has to be the first line and the run has to be closed, by --- or by ..., so a --- with nothing closing it is the rule it has always been; where the two readings collide this reads the metadata, which is what every reader of the notation does, and MawyParseOptions gained frontmatter for a document whose --- at the top means to be a rule. Nothing is thrown away: MdDocument gained a frontmatter, the range it was written at, so an application reads the title out of the source it already has. It is the React package's reading, and the parity check compares the two.

  • A heading writes its own anchor with a trailing {#id}. A heading's id came out of its words and nothing else, so ## What to try first {#what-to-try} drew the braces as part of the heading and named it what-to-try-first-what-to-try — the notation every static site generator reads, and the one document that most wants a hand-written anchor, both lost. A trailing {#id} is now the name the heading is drawn under, the name the outline links to, and not characters on the screen. Both syntaxes read one, the underlined form included, and the same characters a directive's {#id} takes are the ones allowed here, because it is the same notation. Only a lone {#id} is taken: {.warning} and {key=value} mean nothing on a heading and stay as they were written, an anchor anywhere but the end of the line is a sentence, and \{#id} is a heading that says the braces. Two headings asking for the same name are told apart the way two headings with the same words are. The heading's range still covers the whole line, so what replaces a heading replaces its anchor with it. MdHeading gained an id, which is what the heading asked for rather than what it was given — slug is still the name it carries. GitHub does not read this and draws the braces, so MawyParseOptions gained headingIds, defaulting to reading them: a document that has to mean exactly what it would mean there turns it off and gets the braces back as words. It is the React package's reading, and the parity check compares the two.

  • A directive's builder is handed the type of the words around it. A React builder has this for nothing: font: inherit on the element it draws is the paragraph's own type, so a key cap written into a document the reader has set larger grows with it. Here the type is a TextStyle a widget cannot see from inside a span, and a builder had to name a size in pixels and stay that size. The style is now a DefaultTextStyle around whatever the builder returns, so DefaultTextStyle.of(context).style is the paragraph's.

  • A footnote is written from the toolbar, both halves at once. There was no command for one at all. footnote and Mod+Alt+F — where Word and Google Docs put it — write [^1] where the caret is, [^1]: at the end of the document, and leave the caret in the note. Both halves together, because a reference with no note is nothing to the parser: a button that wrote [^1] and stopped would leave the document looking exactly as it did. The number is the first one nothing in the document has taken, counted over every [^…] written in it rather than over what the parser read, so a note nobody refers to yet still holds its own number, and a label that is not a number is in nobody's way. With words selected the reference goes at the end of them. MawyCommand gained footnote, MawyEditorToolbarItem the same, and MawyStrings a footnote. It is the React package's arithmetic, and the parity check compares the two.

  • Shift+Enter writes a hard break. The key was handed to the field, which wrote a bare line ending — one space to a Markdown parser — so a hard break could not be written at all. It writes the two spaces and the line ending a hard break is made of now, <br> in a table cell, whose row is one line of the file, and a bare line ending inside a code block, where every character is the character it is. It is the React package's hardBreak, and the parity check compares the two.

Changed #

  • A code group takes the tab name a fence wrote in brackets after its language. ```python [Python] is how VitePress, Docusaurus and the rest spell a tab's name, and a document written for one of those arrived here with the brackets ignored and its tabs named python and js. The name is read now: {tabs=…} on the group's own line still names all of them at once and still wins where a document wrote both, since it was written knowing what is in the group, and a fence with no name in brackets is called by its language as before. Nothing else reads it — the parser has always kept it as the fence's meta and draws none of it, so this is MawyCodeGroup reading what was already there. It is the React package's component doing the same, and the two name their tabs the same way.

  • The button that copies a code block stands on a plate. It sits in a corner of the block with the code running under it, so a block narrow enough for a line to reach that corner had the icon and the code in one place. It is drawn on a plate of the code's own colour now. The React package blurs what is behind that plate, which a browser does for the price of a declaration; here it would be a BackdropFilter, and a layer per code block is what a long document on a phone cannot spend, so the colour carries it alone.

  • A code group is one surface rather than a box with a box in it. The block inside kept the edge, the rounded corners and the language label it has when it stands on its own, so a group came out as a box in a box with four slivers of the screen showing through at the inner corners, and the tab over the block said the language a second time. A block is told that the box is already drawn now — MawyInsideBox, which is what the React package says with a selector — and draws neither. The row of names is a shade off the code under it, so it reads as the chrome it is rather than as the first line of the block.

  • A rule is drawn down the gap between the line numbers and the source. Grey numbers against the same background as the text beside them read as one column with a wide space in it, and there was nothing on the screen to say the numbers are not part of the document. The line is MawyTokens.border, the same one that goes around the editor, and it is drawn over the field's padding rather than inside it, so it is the height of the field the way the React package's is. gutterWidthFor is where both the column and the rule get the column's width, so the two cannot disagree about where the gap is.

  • The toolbar's heading menu offers all six levels. It offered heading 1, 2 and 3 and body text, so heading 4 to 6 were levels a document could hold and the editor could not write — Mod+4 to Mod+6 did nothing either, since the keys reach only what the menu offers. headingLevels defaults to const [1, 2, 3, 4, 5, 6] now. An application whose screens only go so deep passes the levels it wants, as it always could.

Fixed #

  • No command has anything to do inside a code block, and none is drawn pressed there. Everything in a code block is the characters it is, so there is no formatting in there to turn on or off: ** put around a word in code is two asterisks and a word, and a > somebody typed is a greater-than sign. Run from inside one, every command wrote its markers into the code, and the toolbar read those same markers back and drew its buttons pressed — typing a > in a code block lit the quotation button. Every command but one now does nothing there, and the toolbar draws its controls disabled; codeBlock is the exception, because it is the way out. commandWorks is the rule, and it is the React package's under the same name.

  • A code group that named itself is one tab. A group that wrote a title still held a tab per block in it, so ::: lang js holding a paragraph, a code block and two more paragraphs came out as four tabs called javascript, 2, 3 and 4, with the title beside them saying the language a fifth time. A group that named itself is one answer written out, prose and code together, so the whole of it is one tab under that name; a group that named nothing is still a tab per block. It is the React package's rule.

  • Enter on a quoted line inside a list item carries the quotation down rather than the bullet. A line under an item that is not an item of its own is that item's words run on, and Enter at the end of one carries the item's marker down — which is right for a line of words and wrong for a line the quotation owns. - one with > two under it gave a second bullet and left the quotation behind; it now opens the next quoted line, inside the item where the caret was. It is the React package's rule, and continueList is where both packages ask it.

  • A table wraps its cells into the room it has rather than scrolling sideways. width: 100% inside an overflow-x: auto is what the React package's table is, and a horizontal scroll view hands its child an unbounded width — which a table reads as every column at the width it would like, the whole sentence unwrapped. So the scrollbar was there from the first sentence on, whatever room the table had. The width is worked out now and handed down: the room, or the sum of the columns' longest words where even those will not fit, which is the one case a browser scrolls in too.

  • A bulleted list's dot is the size a browser draws, and hangs where a browser hangs it. • is whatever size the typeface decided to draw it, and on the web that is whatever font the page fell back to; it came out a good deal smaller than the disc beside it in the React package, and sat down the middle of the gutter rather than beside the words. It is a circle three tenths of the type it marks now, its right edge the same fraction of an em from the words as a browser leaves.

  • A list puts a quarter of a line between its items and none at either end. li { margin: 0.25em 0 } in a browser is a quarter of a line between two items and nothing at the ends: two margins between two items collapse into one, and the one at the top collapses out through the list. Both were drawn here, so every list sat a quarter of a line lower than the paragraph above it and its items were twice as far apart as the React package's.

  • The way back from a footnote points the way ↩ points. The icon was the mirror of it — up from the right and then left, where the character it stands in for comes down from the right and then turns left — so the one arrow in the document pointed away from what it goes back to.

  • Home and End go to the ends of the line. On a Mac the platform means the document by them — End scrolled to the bottom and left the caret where it was — so in an editor neither key did what it says. The line is the one the gutter numbers: a paragraph wrapped over three rows is one line with one number beside it, and every offset in this package is an offset into the document rather than into a row whose length is a property of how wide the pane happens to be. Shift extends rather than moves, and under a modifier the key is the platform's own and is left alone. The React package's source answers them the same way.

  • The words typed where a given-up bullet was are their own paragraph, in the middle of a list as at the end of one. Enter on an empty item takes the marker away, and between two items there is nowhere to put the blank line that would part the caret from them: one is already there and a second is the run neither package will write. So the line stayed inside the list, and the letter typed on it was the item above's lazy continuation, drawn at the end of that item. The first thing typed there writes the blank line now and the line becomes the paragraph it looks like, an input method's syllable included, which is asked once the composition is over. It is the React package's keptList, and the parity check compares the two.

  • A hard break stays inside the quotation or the list item it was pressed in. Shift+Enter wrote the two spaces and the line ending and nothing else, so the line it started opened at the left margin: inside > one the second line was only a lazy continuation of the first, drawn quoted while the file said something else, and a > typed after it opened a second quotation rather than carrying on the first. The line now opens with whatever the containers around the caret write on every line of themselves — a quotation's >, the indentation a list item holds its later lines at. Inside a code block that prefix is read off where the block's own line begins rather than out of the line, so a > among the words in there stays code. It is the React package's hardBreak, and the parity check compares the two.

  • Enter on an empty item leaves the list, and the next one stays left. The marker went and the caret was left on the line under the list, which is one line ending away from the last item — and a line of words straight under an item is that item's lazy continuation to CommonMark, drawn at the end of it. So the words typed where the bullet had been joined the item above, and the next Enter, pressed over a line the parser reads as that item's, carried the marker back down: the bullet looked as though it had come back on its own. The marker is given up a blank line away from the list now, which is what makes the caret's line a paragraph of its own. Not where the line above is already blank, and not where it would write a third line ending in a row. It is the React package's partedFrom, and the parity check compares the two.

  • No key is answered while an input method is composing. Korean is composed a jamo at a time and the key that finishes a syllable is an ordinary key, so carrying a list marker down there wrote into a document the composition had not finished changing and put the answer back over the composing run. Every key the source answers waits for the composition to finish now, and Escape, which is what ends one, does not. The React package reads it the same way.

  • A second space in a row and a second blank line are refused, and the editor says which rule it was. Markdown draws a run of spaces as one space, so a document holding three said one thing where it was drawn and another where it was written, with nothing on either to say which the file held. The keystroke that would write the extra character is refused now — drawing it would mean drawing whitespace the parser throws away — and a short sentence appears under the caret and goes again on its own. Two line endings are the blank line two blocks are separated by and still the most a run of them may be, so Enter twice is still how a paragraph is made. A code block, raw HTML, a table's own padding and the whitespace a line opens with are left alone, and so is every document that arrives pasted, opened or handed over rather than typed. It is the React package's crowdedBy, and the parity check compares the two. MawyStrings gained oneSpace and oneBreak.

1.6.0 - 2026-09-15 #

Added #

  • The whole table is deleted from the end of its bar. A last control, in red and set off from the rest, takes out the lines the table is written on and one of the blank lines that set it off, leaving the caret where the table was. MawyTableCommand gained removeTable, MawyToolbarButton a danger that draws it in MawyTokens.caution, and MawyStrings tableRemove. It is the React package's arithmetic, and the parity check compares the two.

  • A line of a table cell written as a list item is drawn as one. - dig<br> - deeper<br>- [x] water in a cell was drawn as its dashes and brackets. Each such line is drawn with a bullet, a checkbox or its number in place of the marker now, a step further in for every two spaces in front of the marker; a line that is only a marker, such as the - of an empty cell, is left a dash. The parser still reads words, as GitHub does, and the React package draws a cell the same way.

  • A column of a table is aligned from the bar beside the caret. Three controls after the column controls write GitHub's colons into the delimiter cell of the column the caret is in, :--- for left, :---: for the middle and ---: for right, keeping as many dashes as there were; the one the column already has is drawn pressed, and pressing it again takes the colons off. With cells selected they align every column the selection covers. MawyTableCommand gained alignLeft, alignCenter and alignRight, and MawyStrings tableAlignLeft, tableAlignCenter and tableAlignRight. It is the React package's arithmetic, and the parity check compares the two.

  • The bar of a table's controls floats beside the caret. It floats a little under the line the caret is on, so in a table longer than the field it is beside the row being written in rather than a scroll away, starting where the caret is across, and over the line where there is no room under it. It lies over the line under the caret, so the controls that add are at the end nearest the caret and the ones that delete or empty cells at the far end, and where the field has no room for the bar past the caret it runs back from the caret with its controls the other way round.

  • A list in a table cell is written as lines of the cell. A cell of a GitHub table holds no block, but it can hold lines that read as a list. With the caret in a cell, bulletList, orderedList and taskList write an item's marker at the start of the line of the cell the caret is on, between the <br>s the cell is written with, and take it off again, and with cells selected they mark or number every line of every cell, numbering again in each. Tab on such a line, past a cell's first, nests it by two spaces in front of its marker and Shift+Tab takes them off. Their buttons stay enabled in a table and are pressed on a cell's lines that are already that list. It is the React package's arithmetic, and the parity check compares the two.

  • The rows and columns of a table are changed for every cell a selection covers. The table commands acted on the cell the caret was in whatever was selected. A selection from a place in one cell to a place in another covers the rectangle of cells between them now, and the bar beside the table acts on as many rows or columns as that covers, says how many, and has one more control that empties the cells. The keys act on a selection the same way. It is the React package's arithmetic, and the parity check compares the two. MawyStrings gained tableRowsAbove, tableRowsBelow, tableColumnsBefore, tableColumnsAfter, tableRowsRemove, tableColumnsRemove and tableCellsClear.

  • The toolbar's table item inserts a table of the size asked for, and a table's rows and columns are changed from a bar beside it. The item was a menu of seven commands, six of which had nothing to act on anywhere but inside a table, and it inserted a table of two columns and two rows and no other. It opens a grid of ten columns and eight rows now, lit from the corner to the cell under the pointer or moved with the arrows, and inserts an empty table of the size pressed, the header counted among the rows; a screen reader is given each size as a button. While the caret is in a table and the source has the focus, a bar of icon buttons with tooltips floats beside the caret, with a row above or below, a column before or after, and deleting the row or the column; a press on it leaves the focus where it was. The keys are unchanged. tableOfSize and tableRangeAt are the React package's commands under the same names, and MawyStrings gained tableSize and tableInsertSized.

  • Tab makes a list item an item of the one above it, and Shift+Tab brings it back. Tab wrote two spaces where the caret was, which in an item's words is two spaces in its words, and under a numbered item two spaces do not nest at all, since 1. is three columns wide. On a list item it moves the item in to where the words of the item above start, or back out to where the item it is in starts, and takes along the lines it runs on over and the items nested in it. A number is counted: 1. for the first item of a list inside another, and the next number of a list the item joins or comes back out into. The first item of a list and an item of the outermost list have nowhere to go, and the key does nothing there. It is the React package's indent, and the parity check compares the two.

Fixed #

  • The editor's preview is as wide as its pane. It kept to the typography's measure, which in preview on a wide screen was a narrow column in the middle of an empty pane. It is drawn with MawyMeasure.full now, as the source beside it is.

  • Deleting a column of a table written without its outer pipes left something that was not a table. a | b over --- | --- lost its last pipe with its second column, so the header became a paragraph and the delimiter row under it a heading's underline. Every line keeps a pipe now, and the first column goes without leaving the space the next one was written after.

  • The blocks do nothing in a table, and the toolbar says so. A cell of a GitHub table holds one line of words, so a heading, a quotation, a code block or a divider has nowhere to go in one, and the commands wrote their marker at the front of the row instead, which ended the table on that line. With either end of the selection in a table they leave the document as it was now, their buttons and the heading menu are disabled, and their keys do nothing. The list commands write the lines of a cell instead; see the entry on a list in a table cell above. The commands are the React package's, and the parity check compares the two.

  • A bare <br> in a table cell is drawn as a line break. Raw HTML is drawn as its characters, and a cell written with two lines, which GitHub tables write as <br> because a row is one line of the file, showed <br> between them. In a cell there is no other way to write a line break, so it is drawn as one there and nowhere else. The React package draws it the same way, and a case in tool/drawn.json holds both to it.

  • The code block command puts the caret inside the block it makes, and takes a block off from anywhere inside it. With nothing selected it came out selecting the whole block it had written, fences and all, which the next letter typed over. The caret is inside now, among the characters it was among. Pressed again from anywhere inside a fenced block, the command takes the fences off rather than writing a second block into the first, and the toolbar's button is drawn pressed while the caret is in one.

  • Enter on the line a list item runs on over carries the list on. An item's words can run over more than one line, after a hard break or wrapped by hand, and that second line has no marker of its own, so Enter at its end was a plain line ending instead of the next item. The marker is read off the first line of the item the parser says the line is in. Code inside an item is still the characters it is.

  • A list, a quotation or a heading can be started on a line with nothing on it. The toolbar's lists, the quotation and the headings left a blank line alone, so pressing one on an empty line did nothing, and a list could only be made out of words already written. On a line of its own each writes its marker now, with the caret after it for the words still to come. A caret, or a selection inside one line, also stays among the words it was among when a marker goes on or comes off, where it came out selected around the whole line and the next letter typed over the marker with the words. The React package's commands do the same, and the parity check compares the two.

  • The name of a button against either edge of a toolbar is drawn on the screen. A tooltip hung from the middle of its button, and the surface switch is against the editor's own edge, so the name of the first mode was drawn half past the edge of the overlay and cut to its last letters. A name that would reach past an edge is hung from the end of its button nearer that edge now, measured with the words it says.

1.5.0 - 2026-09-13 #

Added #

  • links and images say how a link and a picture the document wrote are drawn. A screen carrying documents its readers wrote could keep a link from doing anything by leaving out onLinkTap, and a picture from being fetched through imageBuilder, but a link still looked like one and there was no way to say what should be drawn instead. MawyLinkPolicy.show and MawyImagePolicy.show are what was drawn before and are the defaults. text draws a link as its words, formatting kept, with nothing to tap, and a picture as its description. source draws either as the characters it was written with, set the way a directive nobody claimed is, and hide draws nothing, words and all. Nothing is followed or fetched under any of the three, and imageBuilder is never asked. A footnote's number and the way back from a note are this library's own and stay. The find bar searches what is drawn, so a hidden link's words are not found and a link drawn as its source is found by its address. On MawyViewer and MawyEditor, where they reach the preview. The React package has the same two, and the parity check compares what both find bars find under them.

1.4.0 - 2026-09-13 #

Breaking changes #

  • MawyEditorToolbarItem has new values: undo, redo and table, which are below. A switch over the enum that names every value and has no default stops compiling until it names these too, because Dart checks such a switch for every value the enum has. A toolbar list an application wrote is unaffected, and so is any code that only compares values or builds lists of them.

Added #

  • An application can hand the interface words of its own. strings on MawyEditor and MawyViewer takes a MawyStrings, which is exported now: a locale's words from MawyStrings.of, with the ones that differ changed through copyWith. It is for an application whose translations live in a catalogue of its own, which is a better answer than this package carrying every language that catalogue has. Given, it is every word and locale says nothing, and the editor hands it to its preview. Two sets with the same words are equal, so one built on every build redraws nothing. The class has no public constructor and is final, so a word added to it later is not a breaking change for anybody. The placeholders a few strings carry, %N, %T, %L and %C, are documented on the class. The React package gained the same prop.

  • The quotation, the three lists, the code block, the divider and the image have keyboard shortcuts. The guide said every formatting command had one, and seven of them could only be reached from the toolbar. Mod+Shift+. quotes, Mod+Shift+8, 7 and 9 make a bulleted, numbered and task list, Mod+Shift+E fences a code block, Mod+Shift+K writes an image and Mod+Shift+, a divider. They are the React package's keys, and the reasons for each are in its guide.

  • undo and redo are toolbar items. Undo was a key and nothing else, so an editor on a phone had no undo at all. Both buttons walk the source field's own history, which is still Flutter's and still gathers a run of changes into one step, and each is drawn disabled while there is no step to take back or put back, while the document is read only, and in preview. They are on kMawyEditorToolbar straight after the surface switch. MawyStrings gained undo and redo.

  • A table can be made and reshaped without writing a pipe. The toolbar's new table item is a menu that inserts a table and adds a row below or above, a column after or before, and deletes the row or the column the caret is in, and each has a key: Mod+Alt+T inserts, Mod+Enter adds a row (Shift above, Alt a column, both a column before), and Mod+Shift+Backspace deletes a row (Alt the column). An entry with nothing to act on is drawn disabled, the menu itself is disabled in preview and while the document is read only, and outside a table a key is handed on rather than taken. Each command puts in or takes out one cell per line, so the alignment and what is in every other cell stay as they were, and a table inserted in a quotation or a list item goes inside it. They are the React package's commands under the same names, which the parity check has diffed since they were written. Enter with Mod held no longer carries a list marker down, which is what the React package's source has always done. table is on kMawyEditorToolbar between codeBlock and rule, and MawyStrings gained table and the seven entries' names.

  • headingLevels says which headings the editor's menu offers. The menu offered heading 1 to 3 and body text and nothing else, so an application whose screens write the title themselves, and whose documents start at ##, offered its writers a level they should not use and none of the one below. headingLevels: const [2, 3, 4] offers those three, and Mod+1 to Mod+6 toggle a level only where it is offered, so the keys and the menu agree; a key for a level that is not offered is handed on. The default is const [1, 2, 3], which is what it was, and a number outside one to six is left out. MawyStrings gained heading4 to heading6.

  • An application can insert at the caret and put the focus back from a control of its own. handle takes the new MawyEditorHandle, which the application makes and hands over the way a ScrollController is handed to a scroll view. Its insert(markdown) writes where the caret is, in place of the selection, and leaves the caret after it, and its focus() puts the focus back in the source with the caret where it was. A button beside the editor had no way to do either. Both do nothing in preview, insert does nothing while the document is read only, and a handle no editor has, or whose editor is gone, does nothing at all. The React package's handle does the same.

Fixed #

  • Mod+Z, Mod+Shift+Z and Ctrl+Y walk the source's history outside a WidgetsApp. A WidgetsApp binds those keys to every text field under it, and this package does not require one, so an editor put under a bare Directionality kept a history and gave no key to walk it with. The keys are bound on the source itself now, and under a WidgetsApp they name the same intents its own do.

1.3.0 - 2026-09-12 #

Added #

  • The outline panel names itself, and can be shut from inside it. It drew a list of headings and nothing else: no title, so a panel opened by mistake said nothing about what it was, and no way out except the toolbar button that opened it — which on a narrow screen may be off the end of a bar the reader would have to scroll back along. Both are what the React package's panel has had all along, in the same words and the same order. The name is a line of text rather than a heading, there being no heading here to be; a screen reader still hears it, from the Semantics label the panel already carried.

  • A viewer or an editor can float on the screen rather than sit in a box. frame is which of the two a surface is, and it is on MawyViewer and MawyEditor. MawyFrame.box is what it always was: a surface with a background of its own and the toolbar barred across one end, so a reader can see where the widget starts and the screen around it stops. MawyFrame.floating wraps the document in nothing — no border, no bar across the end, and the toolbar comes out of the column to hover over the text as a rounded group, the way the platform puts its controls over what they act on. It is for a document that is the screen, where a box around the prose is a box around everything and says nothing. The ground under the document stays, because a palette that reaches the text and not what it sits on is half a palette; a tokens whose background is transparent is how an application asks for the document on its own ground.

    MawyToolbarPlacement is which end that toolbar is at, top or bottom, and the find bar travels with it: a find bar at one end with its toolbar at the other is a bar belonging to nothing. The outline stays a column beside the document rather than a card over it, because a card over the document covers the headings it points at — it grows a card of its own instead of the line that divided it from a surface there no longer is, and a floating bar hangs from the document's own column so it never reaches across that panel — and the editor's status line stays the bottom edge of the editor because a count of words is not a control — a floating bar at the bottom of an editor hangs from the panes so the two do not sit on top of each other. Unset, both are what they were.

Changed #

  • MawyViewer.padding unset follows the frame. It was EdgeInsets.fromLTRB(28, 40, 28, 96) and still is under MawyFrame.box; under MawyFrame.floating it is EdgeInsets.zero, because a screen that draws its own margins does not want a second set inside them. A padding that was passed is the padding either way, so nothing changes for an application that says what it wants — and nothing changes at all for one that has not asked for the new frame.

    Worth knowing while reading a screen where this appears to do nothing: the column of prose is capped at the measure and centred in whatever is left, so on a wide screen taking the padding away only widens the gutters and the text lands in the same place. The padding is the padding once the measure has stopped binding.

1.2.0 - 2026-09-11 #

Added #

  • An application can say where a document's relative addresses point. resolveUrl is called for every relative URL a document writes — a picture's source and a link's destination — and what it answers is used as written. A URL in a document is relative to the document, and whatever is drawing it is somewhere else, so ![](./diagram.png) in a file read off a disk had no address anybody could follow. It is on MawyViewer and MawyEditor. Unset, nothing changes.

Changed #

  • A bare address is only linked where its local part is short enough to be one. Sixty-four characters, which is the whole of what RFC 5321 allows. Unbounded, the pattern read to the end of the paragraph looking for an @, gave a character back and looked again, from every position it could have started at — so a run of letters with no space in it, a base64 blob or a hash among them, cost the square of its own length. Sixty-three kilobytes of it took seven seconds and now takes fourteen milliseconds.

Security #

  • A document cannot take the page down by nesting emphasis. Emphasis, strong, strikethrough and links nest inside a paragraph without a container to open, and nothing bounded how far: * written sixteen thousand times is a thirty-two-kilobyte file whose paragraph is eight thousand levels deep, and the stack ran out reading it — and would have run out again drawing it, and in any application walking the tree. A hundred levels now, which is what the containers have had since 1.1.0 and for the same reason. Past it nothing more pairs in that paragraph and the runs left over are the characters they were written with. The two packages gave up at different depths before this, which made it a difference between them as well as a crash.

Fixed #

  • A destination that never closes is read once rather than from every ] after it. [a]( repeated cost the square of its own paragraph: a destination with nothing to close it is read to the end, and read again from every ] written after it, and every character of it cost a regular expression match besides. The characters are read by code now, and the first read that runs off the end works out, for every place a destination could start in that paragraph, where a read from there could first stop — a space, a ) it is not inside brackets for, or a backslash, which counts as a stop because what it escapes depends on where the read began. A read that stops nowhere is refused without being read. A quarter of a megabyte went from a hundred seconds to 90 milliseconds.

  • Every string this parser builds is built in a buffer. A Dart string is immutable, so appending to one copies everything held so far — and a paragraph's text, a reference label, a link destination and its title are all read a character at a time, as are the runs of text merged into one node at the end of a line. Each of those cost the square of its own length. Three hundred kilobytes of prose went from a second and a half to 43 milliseconds, a sixty-three-kilobyte reference label from forty-six seconds to 102, and a megabyte of [a]( repeated from over a minute to 328. The React package needs none of this: a JavaScript engine already does it behind +=.

  • A paragraph that is one long line of emphasis is read at its own size. The chunks a line is read into were a list, and what this algorithm does to them is take a span out of the middle and put one node in its place — once for every pair of delimiters and once for every link — which in a list moves everything after the cut. They are a linked list now, and the delimiters that pair off leave a hole rather than being taken out, so neither costs anything. A hundred and twenty-five kilobytes of *a* repeated went from forty seconds to 94 milliseconds.

  • A paragraph that is a list of links is read in the time a list should take. Every link that closed searched everything read so far, once for each delimiter run in it, so the cost grew with the square of the paragraph. Forty-seven kilobytes of links went from 39 milliseconds to 8, and the same shape in the React package from 1.4 seconds to 5 milliseconds.

  • Two footnote definitions on adjacent lines are two notes. [^a]: … with [^b]: … on the line under it came back as one note whose text ended in the characters of the second, and the sentence pointing at the second showed its brackets. A definition's paragraph was continued lazily into the definition below it, as a paragraph is continued by any line that follows it; a line opening the next definition ends the one above it now. A blank line between them was the way round it and still reads the same.

  • A quotation and a footnote are drawn with the whole of what the document is drawn with. Both set their own body text — a quotation's is muted, a note's is smaller — and both built a second rendering context that had lost some of the rest of it. A code block inside either was never coloured, a directive inside either was drawn as nothing at all rather than as the characters the author typed, a picture inside a note was fetched by the viewer even where imageBuilder said the application would draw it, and neither the find bar's marks nor a footnote's two taps reached inside a quotation.

1.1.0 - 2026-09-06 #

Breaking changes #

  • MawyViewerAnchors.keyFor is gone. places reads the viewer's own record of how tall every block was laid out, so no GlobalKey is handed out any more. It was documented as the viewer's own, and nothing else in the class changed.

Added #

  • A footnote's number goes to the note, and the note goes back. Both halves of a footnote are links now, and the document scrolls itself to the other end.

  • The editor's palette can be one the application holds. colorScheme had onColorSchemeChange but no controlled form, so an application with its own light/dark switch could not drive the editor from it.

  • A picture can be drawn by the application instead of fetched by the viewer. The picture is handed over whole, with the address the allowlist already checked, the alt text and the title. Unset, nothing changes.

Security #

  • A document cannot take the page down by being deeply nested. Containers nest a hundred deep now and no further, so a four-kilobyte file of > repeated a couple of thousand times no longer runs the stack out. The two packages gave up at different depths before this, which made it a difference between them as well as a crash.

  • Inline code is drawn in the accent colour, in a box the height of the words. The pair clears 5.5:1 on that box in either theme. The box was the height of the whole line, because a run inheriting the paragraph's line height fills the paragraph's line. The React package draws both the same way.

  • The footnotes name themselves to a screen reader rather than writing the word across the page. The section carries the name instead, and each note keeps a node of its own rather than all of them being read out as one string.

  • The status line, the split-pane arithmetic and the viewer's find are diffed against the TypeScript ones, as part of the parity check. Widening it found that countWords had never been compared on unspaced text, so tool/corpus.json now ends with a document of Han and kana. The new fixtures are tool/scrolls.json and tool/finds.json. Nothing in this package changed.

  • Taking the link reference definitions off a paragraph is one pass over it. The scan starts where the last one ended, instead of handing the pattern a fresh copy of everything left.

  • Whether a toolbar button is pressed costs the size of the answer rather than the size of the selection. Each button asks whether its command is in force every time the caret moves, and each question copied the whole selection out of the document. The wrap is read at the edges of the selection now, and the line markers a line at a time.

  • The viewer draws its document again only when the document changed. The drawing is kept until something it is made of changes, so a pointer moving over a code block or a reader passing a heading no longer rebuilds every block and span. A directives map handed over inline is compared by the builders it names, and an onLinkTap closure written in place is read as "a link does something" rather than as a new answer.

  • Moving the caret stops redrawing the document beside it. In split, the preview is kept as the same widget while nothing it draws from changes, which is a subtree Flutter does not visit.

  • The viewer searches when the question changes rather than whenever it draws. The answer is kept until the query, the case switch or the document changes, instead of walking every run of text on each redraw.

  • The outline finds where the reader is by halving rather than counting. Headings come down the page in the order they are written, so which one is at the top of the view is a binary search rather than a walk from the first.

  • Typing with the find bar open costs the same whatever it found. The matches are cut against the lines once, in one walk down both, and each line is handed only its own. The same change the React package got.

  • Numbering the footnotes costs what the document has rather than the square of it. Each name is written down as it is given out, instead of asking every note already numbered whether it had taken the name.

  • Finding without case sensitivity folds the document in one go. A text with no character that changes length in lower case is folded in one call, and the answer is the same either way.

  • The drawn document is built where it can be seen. It is a lazy list now: measured against a document of two thousand four hundred blocks, the viewer went from twenty-five thousand render objects to five hundred, and laying it out again after a change of type went from a second to a tenth of one.

    It starts at four hundred blocks. Under that every block is built, which keeps a selection whole, and four hundred is past a long README, a reference page or a chapter. Over it a selection reaches the three screens either way the list keeps, and the toolbar's copy button takes the whole document from the Markdown rather than from the page.

  • Where a block of the drawn document sits is written down rather than looked up. Every block reports the height it was laid out at, and the running total is where each one begins. The outline measures without touching the document, and places now answers for every block rather than only the ones on screen.

  • The source field is coloured where it can be seen rather than end to end. Only the lines near the view are read for syntax, and everything else is handed over in one span. The field still lays the whole document out, so the caret, the selection, the scroll extent and the line numbers are unchanged, and a document under six hundred lines is coloured as before.

Fixed #

  • A picture no longer paints over the paragraphs around it. Every line is held to the height of a line of text, which keeps a paragraph of Hangul and one of Latin the same height, and a picture centred on such a line painted out of both ends of it. A line with a widget on it grows to fit it now, and every line without one is as even as it was.

  • A picture the document carries itself is drawn. The URL policy allows a data: image, and the renderer handed it to Image.network, which can only open one on the web. The bytes are read out of the URL and drawn from memory, once per picture rather than once per build.

  • A picture is decoded at the size it is drawn at. A photograph four thousand pixels across was decoded at four thousand to be shown at six hundred, which is forty-eight megabytes for one picture. It is decoded at the width the page has for it.

  • A hand on a trackpad scrolls as smoothly as it moves. Easing every small movement of a two-finger gesture over a seventh of a second turned it into a run of little starts. On the web both arrive as the same event, so the size of the movement now tells a trackpad from a wheel.

  • The line numbers keep up with the lines. The column repainted only when the text or the type changed, so rewrapping left the numbers behind; it walked down from the first line to find the first number to draw; and it leaked a TextPainter per number per frame. The first line on screen is found by halving now, one painter draws the column, and it repaints whenever the field it reads from might have moved.

  • A copy the platform refuses says so. The clipboard can refuse, and nothing was listening, so the button appeared to have worked. It says "could not copy" for the same moment it would have said "copied", in both copy buttons.

  • A code block's copy button holds its label for the same moment however often it is pressed. It counted with a delayed future that cannot be called off, so a second press was cut short by the first press's timer. The toolbar's button was fixed for this a release ago.

  • The editor stops reporting the document it was given as a change. The field reports a caret move as well as a text change, so an editor handed a document and clicked into once called onChange before anybody had typed. It says nothing until the text is different, and nothing at all about a value the application set itself.

  • The caret stays where it was when the application hands the document back changed. A controlled editor whose value came back trimmed or normalised moved the caret to the end of the document. It is kept where it was, clamped into a document that got shorter.

  • A link keeps the recognizer it was given. One was made for every link on every build and the previous build's were disposed while the spans still held them. Each link keeps the one it has now, and unused ones are let go of after the frame.

  • Reading the document happens when it changes rather than while the frame is being built. The lazy parse sat inside build, along with everything it threw away when the document turned out to be new, including telling the application's anchors object to forget where every block was. It is done when the text or the options change now. The keys the viewer kept per block were never thrown away at all, so a document replaced by a shorter one left keys behind.

  • A case-insensitive search answers the way it does in the React package. Both packages leave a letter alone when its lower case is not one character in the same place, because every offset a search reports is an offset into the document. Dart folds İ one character for one where JavaScript does not, so a search for istanbul found İstanbul here and nothing there. It is left as written on both sides now. Folding it properly needs a Unicode case folding table, which neither package ships.

  • Two footnotes can no longer be given the same name. Where a document had already written out the name a slugged label would take, both got it. A name that is taken is counted past now until one is free.

  • A document somebody has clicked into scrolls with the keyboard. The arrows and Page Up/Page Down did nothing, because only a WidgetsApp scrolls a focused box here and this package does not require one. A reader who asked the platform for less movement gets the jump rather than the glide.

  • The bar between the panes of split is something a finger can hit. It was five pixels and is thirteen now, with the same one pixel drawn down the middle.

  • A picture the author described is named, and one they did not is skipped. ![](…) has no description, which in Markdown means decoration, and it was left in the tree as an image with no name for a screen reader to stop on.

  • A heading in a drawn document is a heading to a screen reader, and says which level. It was text at a larger size, so moving through a document by its headings did not work. The React package writes an <h1> and gets this for nothing.

  • The formatting commands have the keyboard shortcuts the guide said they had. Mod+B, Mod+I, Mod+K, Mod+E, Mod+1/2/3, Mod+0 and Mod+Shift+X all reach the source surface now. The toolbar was the only way to any of them.

    The table is src/components/editor/MawyEditor.tsx's, under the same name. Mod+S is the one line still not answered here, there being nothing to save to, and undo is Flutter's own.

  • Escape and then Tab leaves the source surface. Tab indents there and was the only thing it did, which is a keyboard trap for anybody who cannot use a pointer. One Escape arms the way out and anything else typed disarms it again, which is the rule the React package, CodeMirror, Monaco and GitHub's editor all use. The surface is named to a screen reader now and says how to leave, in MawyStrings.sourceEscape.

  • The copy button says it copied for the same moment however often it is pressed. The run of time putting the label back could not be called off, so a second press was cut short by the first one finishing.

  • Enter stops carrying a definition marker down in an editor that does not read definition lists. continueList takes parse.definitionLists now, and defaults to reading them.

  • The status bar says the column in the same characters as the count beside it. The column was counted in UTF-16 units and the selection in code points. Both are code points now.

  • Finding without case sensitivity reports the match where it is. A letter whose lower case is longer, such as İ, made the folded copy longer than the document and shifted every match after it. The folding keeps the length now, at the cost of İ matching only itself.

  • A heading toggles off over a selection with a paragraph break in it. Blank lines were read as lines that are not headings, so the toggle never turned off. They are read past now and left alone.

  • Making two paragraphs a list no longer leaves an empty item between them. A blank line inside a list is left alone now, which is what the ordered list already did.

    A quotation keeps its marker on the blank line, because a quotation without one there is two quotations rather than one with a paragraph break in it. Both packages, and the parity check diffs them.

1.0.0 - 2026-09-02 #

The entries below are a long list of work, and a long list is a minor version. What makes this a major is the promise that comes with the number: from here the exported API is under semantic versioning, so a name that goes away or changes shape waits for another major. The one break in this release is MawyTokens, under Changed.

Added #

  • The viewer can be searched too. A find button on its toolbar, Ctrl+F (Cmd+F) while it has the focus, every match marked as the query is typed and the one being stepped through marked apart — the same bar and the same colours the editor has, without the second row, there being nothing in a viewer to put anything in place of.

    What it searches is the text the document draws, which is the whole difference between this and the editor's. **bold** puts six characters in the source and four on the page, and a reader looking for bold is looking at the page: so bold is found inside the bold, and ** is found nowhere. A fenced code block is not searched — it is drawn as the highlighter's own spans, and cutting a mark into those would mean cutting every one of them — and a match cannot straddle two runs, so hello is not found across he**llo**.

    The button is the find toolbar item and the shortcut comes with it: a viewer whose toolbar leaves it out leaves Ctrl+F to the browser, which is the right answer for a viewer that fills the page. Taking it is worth doing for one inside a pane of its own, which the browser's find scrolls past rather than into.

  • Finding marks every match at once, and marks the one you are on apart from the rest. Before, the only thing on the page saying where a match was was the selection sitting on it — one match, and only after pressing next. Typing a query told you how many there were and nothing about where.

    Every match is now painted as the query is typed, in a wash of yellow, with the one being stepped through in a stronger orange. Which turns the count beside the field into something you can check against the document rather than take on faith, and makes "next" a thing that visibly moves.

    In the source pane it goes into the spans the controller hands the field rather than into the field's own selection, so a match inside a heading or a link keeps the colour the highlighter gave it and gains a background. MawyTokens.find and MawyTokens.findCurrent are the two colours, and they are palette entries like every other colour here.

  • A mouse wheel arrives over a few frames rather than all at once. Flutter answers a notch by putting the offset where the notch says on the next frame and drawing nothing in between, and it is the nothing in between that reads as hard: every browser and every native application on the platforms this is read on animates the same distance. A second notch while the first is still arriving adds to where it was going rather than starting again from where it has got to, which is what keeps a run of them feeling like one movement.

    Nothing to turn on, and a reader who asked the platform for less movement is given the jump back — the same answer the stylesheet gives under prefers-reduced-motion. The source surface keeps the platform's own wheel: a text field scrolls itself rather than being scrolled by something around it, and there is nowhere between the two to stand.

  • In split, the preview scrolls with the source. It did not move at all: the two panes were two scrollers with nothing between them, and a writer who scrolled the source was reading one document and looking at another. The React package has lined the two up since it had a split at all, and the guide has been describing behaviour this package did not have.

    To the block rather than to the same fraction of the way down the file, which is the same answer and the same arithmetic: src/editor/scroll.dart is src/internal/scroll.ts in Dart, function for function. A fenced block is twenty lines of source and twenty lines of page, a paragraph is one long line of source and six of page, and the fraction through the file is not the fraction down the page — so the panes are lined up at the places they can agree on and run straight between them.

    The measuring is the half that cannot be shared, because a browser reads a bounding box off an element and this reads a viewport. MawyViewerAnchors is the new piece: hand one to a viewer and it keeps a key on every top-level block, ask it where they are and it measures them. It is what the editor uses, and it is public because an application lining any second view up with a drawn document needs exactly this and has no other way to get it.

  • The document is text a reader can select and copy. Nothing in one was selectable, in the viewer and in the editor's preview alike — dragging across a paragraph took nothing and there was no way to get a sentence out of a document but to retype it. That is the cost of drawing a document as widgets rather than as markup, which is also what makes the safe default free: a page of widgets selects nothing unless it is put inside a region that says so.

    Dragging selects, a double tap takes the word under it, and Ctrl/Cmd+C copies. No handles and no context menu — both of those are Material's or Cupertino's, and a package that draws its own everything else should not pull in a toolbar it did not design — so the copy keys are written out here for the same reason Enter and the space bar are.

  • The outline panel is reachable by a keyboard, and following an entry takes the focus with it. Every entry is its own tab stop and is pressed with Enter or the space bar, the way the React package's <button>s in an <ol> are — not the toolbar's one stop and a set of arrows, because a list of a document's headings is not a row worth learning a second way of moving through.

    It was the one piece of the accessibility work that got left behind: the toolbar, the menus, the sliders and the reset links were all made to take the focus, and the entries stayed a GestureDetector — which is a panel a keyboard can open and cannot then use.

    Following an entry now moves the focus as well as the scroll, so the next Tab carries on from the heading rather than from the panel. The heading is skipTraversal, which is the web's tabIndex = -1 said the other way round: somewhere the focus can be put, and not somewhere Tab stops on the way past. That was the last thing the viewer guide said this package did not do yet, and the sentence is gone.

  • The bar between the two panes of split is something to take hold of. The React package's, said in Flutter's terms and landing in the same commit: drag it, or focus it and use the arrows — Shift for a bigger step, Home and End for the ends, Enter or a double tap for half and half again.

    A Semantics slider rather than a button, which is what it is, and it says its value as a percentage. It stops well short of either edge, because a pane pushed to nothing is a pane nobody can get back. MawyStrings.divider is the only thing added.

  • The source surface answers the pointer. Dragging across it selects, a double tap takes the word under it, a triple tap takes the line, and a long press on a touch screen raises the handles. None of that worked: an EditableText on its own puts the caret where it is tapped and stops there, and everything else a text field does with a pointer is TextSelectionGestureDetectorBuilder, which TextField builds around its own field and this did not build around its own.

    It costs nothing this package has refused elsewhere — the builder is in package:flutter/widgets.dart rather than in Material. rendererIgnoresPointer goes with it, because the detector and the renderer both want the gesture and two things reading one drag is a caret that jumps to where a selection was meant to start.

    The force press stays off. What it opens is a magnifier and a toolbar and both of those are Cupertino's, and a gesture that starts something this package cannot finish is worse than one that does nothing.

    Everything the toolbar does to a selection was reachable only from the keyboard until now, which is most of what the toolbar is for.

  • The highlighter knows Dart, which is the language this package is written in and the one it could not colour. Every Flutter example on the documentation site is a fenced dart block and every one of them was drawn plain. Doc comments, annotations, strings written across three quotes, and the six lowercase type names read as types rather than keywords. Both packages in the same commit, and tool/parity.dart diffs every token the two produce over a piece of it.

  • One more of CommonMark, and the number moved from 639 to 640. The React package's parser change, mirrored here in the same commit: a blank line loosens the list it is in when it is past the end of one of the item's blocks and inside none of them, rather than between two of them — which is where an item ending in a reference definition used to fall out.

  • One more of CommonMark, and the number moved from 638 to 639. The React package's parser change, mirrored here in the same commit: a lazily taken line cannot cut its paragraph short. A container hands one over with its indentation gone, and a bullet four columns in — not a marker where it was written — used to open a list nobody wrote.

  • Three more of CommonMark, and the number moved from 635 to 638. The React package's parser change, mirrored here in the same commit: all three are the lazy continuation. Only a paragraph is continued across a line that forgot its > — a fence the quotation opened, or code it indented, is waiting for nothing — and a line taken that way is the paragraph's next line and not a setext underline.

  • One more of CommonMark, and the number moved from 634 to 635. The React package's parser change, mirrored here in the same commit: a document that ends in a newline has that many lines and not one more, and the blank line the reader used to invent at the end of one made a fence the document never closed a line taller than its code is.

  • Finding text, and replacing it. Mod+F opens a find bar over the source, and the toolbar has a find button that does the same thing. Enter goes to the next match, Shift+Enter to the one before, Escape closes the bar and gives the focus back to the document, and whatever was selected on one line is already in the box when it opens.

    It is there because a platform's own find reaches a page of text and not the inside of a text field, and the source surface is one — the same justification the React package's has, which is the only one either of them needs.

    The arithmetic is src/internal/search.ts in Dart, function for function, and tool/parity.dart diffs the two over tool/searches.json: whether aa in aaaa is two matches or three, which one next goes to from where the caret is, and what replace all does to a document are decisions, and they are the same decisions in both. Plain text and never a regular expression, for the reason written down over there — a Markdown document is full of *, [, . and +.

    findMatches, matchFrom, replaceMatch, replaceAll and MawyMatch are exported, like the commands are, for an application that would rather drive them from its own chrome.

  • MawyEditorToolbarItem.find, which is on the default toolbar. open and save are still the application's: a file picker is a plugin rather than a widget, and which one an app has already chosen is not a decision a Markdown editor should make on its behalf.

  • The whole Latin-1 block of character references, which is four more of CommonMark and takes the number from 630 to 634. The React package's table change, mirrored here in the same commit: ninety-seven names — &ouml;, &eacute;, &szlig;, &frac12; and the rest — read everywhere the specification asks for a reference.

  • One more of CommonMark, and the number moved from 629 to 630. The React package's parser change, mirrored here in the same commit: a numeric character reference is at most seven digits, or six in hexadecimal, so &#87654321; is text rather than a reference to a code point that does not exist.

  • Two more of CommonMark, and the number moved from 627 to 629. The React package's parser change, mirrored here in the same commit: a no-break space is not one of the five characters the specification calls whitespace, so it does not separate a destination from a title; and a thematic break wins over a list item inside a list as much as outside one.

  • Two more of CommonMark, and the number moved from 625 to 627. The React package's parser change, mirrored here in the same commit: closing a link deactivates the [ openers to its left and no longer the ![ ones, so ![foo [bar](/url)](/url2) is an image whose alt text is foo bar rather than a sentence with a link in it.

  • Four more of CommonMark, and the number moved from 621 to 625. The React package's parser change, mirrored here in the same commit: a link reference definition's label may run over more than one line, may not hold an unescaped bracket, and may not be nothing but whitespace — and a setext underline over a paragraph that was nothing but definitions is a line of text rather than an underline.

  • Three more of CommonMark, and the number moved from 618 to 621. The React package's parser change, mirrored here in the same commit: an attribute name is the specification's rule rather than "anything that is not a space or a quote", so <a h*#ref="hi"> is a sentence about a tag rather than a tag; and <!--> and <!---> are comments in their own right rather than the start of one that runs to the next -->.

  • A viewer takes a palette of its own. MawyViewer.tokens and MawyEditor.tokens are the React package's --mawy-* custom properties said in Dart, and the whole of what theming is here:

    MawyViewer(
      value: document,
      tokens: (Brightness brightness) =>
          MawyTokens.of(brightness).copyWith(accent: const Color(0xFFB8005C)),
    );
    

    It is a MawyTokensBuilder rather than one palette because a viewer settles on its brightness after it has been handed everything else — from colorScheme, or from the platform where that is system — and a document that follows the platform has to be able to follow it in both palettes rather than only in the one it opened on. An editor passes what it is given to its preview, so an editor and the document it is editing are never two palettes.

  • MawyTokens.copyWith, which is how one of those is written: start from MawyTokens.of(brightness) and name what differs. Thirty-one arguments to change one colour is not a palette anybody writes twice.

  • The three pieces of accessibility this package was missing, and they were the three the React package had:

    The toolbar is one tab stop. A keyboard enters it once, leaves it once, and moves between the controls inside it with the arrows — Home and End for the ends of the row, Enter and the space bar for whichever control the focus is on. Eleven buttons above a document used to be eleven things to step over on the way to reading it, and now they are one. It is src/internal/roving.ts in Dart, in src/internal/roving.dart, because there are two toolbars here and two copies of a focus model drift into two different keyboards.

    A menu closes on Escape and gives the focus back to the button it came from. It also opens with the focus already in it, which is the one place this differs from the React package and is not a preference: a panel there is the next element after its own button, and a panel here is put up through the Overlay, which is nowhere near that button in the order Tab walks.

    Animation is dropped where the platform asks for less of it — MediaQuery.disableAnimationsOf, which is the same setting the stylesheet reads as prefers-reduced-motion. The toolbar's and the outline's transitions become instant, and following an outline entry puts the reader at the heading rather than travelling there.

  • An editor. The Markdown source with its syntax coloured, a live preview beside it, a formatting toolbar and a status bar that counts:

    MawyEditor(defaultValue: '# Hello', onChange: save);
    

    Three surfaces rather than the React package's four, and the missing one is worth saying out loud. wysiwyg there draws the document and edits it where it is drawn, which rests entirely on contenteditable: a browser telling a component what somebody tried to do to a tree, so the component can refuse it and change the Markdown instead. Flutter has no such thing — an EditableText owns a string — and drawing a document that is also a text field would mean a second model of what the document is. A second model is a second opinion about what a document means, and the two disagree the first time anybody writes something unusual. So plain, split and preview, and the drawn surface stays a viewer.

    Everything else is the React package's, and provably: the formatting commands, the colouring of the source and the counts along the bottom are the same functions under the same names, and tool/parity.dart diffs all three on every change. Enter on a list item carries the marker down and gives it up on an item still empty; Tab and Shift+Tab indent by the two spaces a nested item needs.

    Colouring the source is the one place this package has the easier job. The React editor lays a transparent <textarea> over a coloured copy of the same text and keeps the two in step, because a browser gives no way to colour what is inside a text field; a TextEditingController is simply asked for the spans it wants drawn.

  • MawyEditor, MawyEditorMode, MawyEditorToolbarItem and MawyEditorStatusItem, along with kMawyEditorModes, kMawyEditorToolbar and kMawyEditorStatus — and the commands themselves as runCommand, commandActive, continueList, indent and EditState, for an application that would rather drive them from its own chrome.

  • Six more of CommonMark, and the number moved from 612 to 618. The React package's parser change, mirrored here in the same commit: all six are about which lists are loose — an empty item does not loosen its list, an item may begin with at most one blank line, and a blank line loosens the list it is in rather than every list around it.

  • A syntax highlighter, and a code block that uses it. mawyHighlighter is the React package's src/highlight.ts in Dart — the same grammars, the same rules, the same approximations — and tool/parity.dart now diffs every token the two produce over a piece of every language either of them claims. A code block coloured in a browser is coloured the same way in an app, which is the promise the parser already made and the one this makes now.

    MawyViewer(value: document, highlight: mawyHighlighter);
    

    Nothing is coloured without being asked. A highlighter is the largest thing a Markdown renderer can be made to carry and most documents have nothing to colour, so an application that never names one never carries the tables: a Dart build drops what nothing references, which is what this package has instead of the React package's separate entry point.

    It is tokens rather than markup, like everything else here — what a highlighter hands back is text and names, and this package decides what each becomes, so nothing reaches the screen as markup of any kind. The tokens are joined together and checked against the code they claim to be, and a block that does not add up is drawn plain.

  • MawyHighlighter, MawyCodeToken and MawyCodeTokenKind, exported from package:mawy/mawy.dart like the rest of the vocabulary. They live in src/code.dart rather than src/types.dart and import nothing: the parity check runs the highlighter under the plain Dart VM, and a library that reaches package:flutter/widgets.dart cannot be compiled by one.

  • Eight more colours on MawyTokens — highlightComment, highlightString, highlightNumber, highlightKeyword, highlightType, highlightFunction, highlightVariable and highlightPunctuation — which are the React package's --mawy-hl-* custom properties, value for value.

  • Seven more of CommonMark, and the number moved from 605 to 612. The React package's parser change, mirrored here in the same commit: a destination, a title and a fence's info string read their escapes and character references; a backtick fence whose info string holds a backtick is not a fence; and an escaped bracket is a bracket a shortcut reference's label may hold. tool/parity.dart says the two trees are still identical, which is the only thing that makes "one parser shipped twice" true rather than intended.

  • The palette's faintest text now meets WCAG AA. foregroundSubtle was #8B8B96 in the light tokens and #77778A in the dark, which is 3.4:1 and 4.1:1 against the backgrounds it is drawn on — under the 4.5:1 that body text needs. It is #70707B and #87879A now. This is the React package's change, mirrored: the two palettes are one palette, value for value, and a colour that moved there had to move here.

  • How much CommonMark, as a number. The React package's parser answers 605 of the specification's 652 examples, and this parser is that parser: tool/parity.dart diffs the two trees over every awkward case and every Markdown file in the repository, so the suite is run once rather than twice. What the remaining 47 are, and why, is written down beside the test over there.

  • Directives — a way for a document to carry a construct this package does not know about. The parser reads a shape and stops there: :::name[label]{key=value} … ::: around blocks, ::name[label]{attrs} on a line of its own, and :name[label]{attrs} inside a sentence. What each one means is the application's, through directives:

    MawyViewer(
      value: document,
      directives: <String, MawyDirectiveBuilder>{
        'callout': (BuildContext context, MawyDirective directive) =>
            Callout(kind: directive.attributes['kind'], children: directive.children!),
      },
    );
    

    A builder is handed the name, whatever was written in {…} — with {#id} arriving as id, {.a .b} as class and a bare name as a flag — the [label] already drawn as an InlineSpan, a container's blocks already drawn as widgets, the range it was written at and the characters it was written with. Which keeps the safety story exactly where it was: the application composes widgets, and there is no markup on the path from the document to the screen. An inline directive is placed in the sentence as a WidgetSpan. A name nobody registered is drawn as the characters it was written with, the same answer raw HTML gets, because a screen that was never told what a construct means should show what the author wrote rather than quietly lose it.

    This is the React package's parser change, in Dart: the same syntax, the same tree, and tool/parity.dart diffs the two over the awkward cases and every Markdown file in the repository. Two rules are narrower than the remark-directive extension's, and both are about not changing what an existing document already said: the colons must be followed immediately by the name, so ::: tip with a space is the paragraph it always was; and an inline directive must carry a label or attributes, so Note: and 12:30 and :warning: stay what they are.

  • MawyDirective, MawyDirectiveBuilder and MawyDirectiveKind, exported from package:mawy/mawy.dart like the rest of the vocabulary.

  • The headings panel marks the heading the reader is at. It marked nothing at all, so a panel open beside a long document was a list with no answer in it — the React package's has had the rule down the leading edge since it had a panel. Measured from the document's scroll, and pinned to whatever was pressed until the reader scrolls somewhere of their own, which is the same rule and for the same reason: following an entry is an animated scroll that passes over every heading between here and there.

  • The source surface numbers its lines, the way the React package's has since it had a source surface — an editor is a place errors are reported by line, and a line nobody can name is a line nobody can be sent to. MawyEditor.lineNumbers turns it off, and is on by default there too.

    A wrapped line is two rows on the screen and one number down the side, which is the awkward half and the reason this took a while: the numbers are painted from the caret rects the laid-out field already reports rather than from a second layout of the same text, so the two cannot drift — there is only one of them.

  • A toolbar button says its name under the pointer. An icon with no word beside it is a control nobody can name, and until now only a screen reader was told. It is the React package's tooltip in the same palette and the same shape, and it is hung off a pointer entering the button rather than off the focus highlight — those are two different questions, and a finger raises neither.

  • MawyEditor.onOpen, and an editor holding nothing offers to be filled. The open button is on the default toolbar now, and where the document is empty the preview stops being a blank rectangle and becomes the way in — the React package's empty state, in the pane the document is going to appear in. Both are drawn only where onOpen was given, because a control that cannot do what it says is worse than none.

    It is the button and not the picker. Reading a file is still a plugin and still the application's, which is what this package has said about open and save from the start; what is new is that the editor draws the control and says when it is worth offering, instead of leaving an application to build its own. MawyStrings gains openFile, emptyTitle, emptyHint and emptyAction, which the React package already had.

Changed #

  • MawyTokens has two more colours, and its constructor asks for them. find and findCurrent are what the find bar paints over what it found. Every field on that class is required and these are no exception, so an application that builds a palette with MawyTokens(...) from nothing has two more arguments to pass; one that starts from MawyTokens.of(brightness) or .copyWith(...), which is nearly everybody, has nothing to change. This is the sort of thing a major version is for, and there is not another one due.

  • The editor's theme control is a menu rather than a button that cycles. Light, dark and the platform's, all three at once with a tick beside the one in use — which is what the viewer's toolbar here already offered and what the React package's editor has always offered. A button that cycles is a button pressed twice to reach the value on the other side of the one you did not want, and the list will be longer than three the first time this library ships a palette that is neither light nor dark.

    MawyToolbarChoice is the panel behind it, public now, because there are two toolbars in this package and the list a theme is chosen from should not be two lists that resemble each other.

  • The headings panel is called "Contents". The React package's change, in the same commit and for the same reason: "Outline" is what the thing is to whoever wrote it rather than what a reader is looking for, and the Korean has always said 목차. MawyViewerToolbarItem.outline is unchanged, because that is an application's API.

  • An outline entry says its heading once. The panel named each entry after the heading it points at and then drew that heading inside it, and a screen reader handed both read the words out and read them out again — Second, Second, button. The drawn words are the drawing now, and the name is the name. The React package's entry is a <button> with the heading inside it and has always said it once, which is what makes this a difference between the two rather than a preference.

  • MawyTokens compares on every colour rather than on six of them. Six was enough while the only palettes in existence were this package's own two, and became wrong the moment an application could build a third: two palettes differing in nothing but their alert colours called themselves the same palette, and a viewer handed the second one would not have redrawn.

  • The editor's toolbar is the React package's toolbar, control for control and in the same order. The two had drifted into different arrangements of the same buttons, and the three heading levels were three buttons in a row rather than one menu — which is four answers to one question drawn as four questions. heading opens a list now, with body text on it, the way the browser's does.

    The panels are hung from the leading edge of the button that opened them and flip to the trailing edge only where that would run them off, which is the rule the React package's menu already followed. It was not one this package needed while every menu lived at the right of a viewer's toolbar; the first menu on the left of an editor's hung off the window.

  • The source surface's placeholder says where the typing goes, rather than naming the format the reader is already looking at. The React package's placeholder changed with it, so the two still say the same thing.

Fixed #

  • Enter in the find field goes to the next match. It did nothing at all on the web, and took the focus with it everywhere else — so the second Enter was a newline typed into the document, and every keystroke after that was an edit somebody had asked for a search.

    Two things were wrong and the web one is worth writing down. A Flutter view puts a real DOM input under whichever field has the focus, and the browser keeps Enter for itself: what arrives in the framework is the field's input action, never a key event, so the bar's key handler was listening for something that never came. It listens for the action now — and for TextInputAction.unspecified, which is the only one the framework does not read as "finished": every named action gives up the focus and asks the platform for a fresh input, which is the opposite of what pressing Enter in a find bar means.

    The selection still moves to the match, and the document still picks up from there the moment the bar is closed. What it no longer does is take the focus while the bar is open, which is what the marking above is for: the match is shown where it is rather than shown by being selected, and the field the query is being typed in keeps the keyboard.

  • A task list's box sits on the middle of its first line. It was nudged down by a number written by hand, which is right at one line height and above the text at every other — and the default is not that one, so every task list in the package was drawn with its boxes riding high. It is half the difference between the line box and the type in it now, which is where the browser's vertical-align puts the React package's.

  • Every menu on both toolbars opens. They did nothing at all, and in a release build they did nothing loudly: the panels are raised into an [Overlay], Overlay.of asserts in debug and throws a null check with asserts stripped, and an application that has neither MaterialApp nor routes has no overlay for them to go into. That is not an exotic tree — it is what an application that wanted neither Material nor Cupertino writes, and this package's own gallery is one, which is why every Flutter preview on the documentation site had a toolbar where nothing happened.

    A viewer and an editor bring an overlay when they cannot find one, and use the application's when there is one. The typeface, the three sliders, the column width and the theme are all a menu, and all of them were affected.

  • The editor's toolbar is the width of the editor. A Column centres its children unless it is told otherwise, so the bar was as wide as its buttons and floating in the middle of the window, with the rule under it stopping where the buttons stopped. The viewer's was already full width by accident of what is inside it, and now both say so.

  • A focused control is drawn with a ring and not a block. The focus indicator was a BoxShadow spread two pixels behind the button, and a shadow is a filled shape — behind a button whose own background is nothing, what it draws is a solid rectangle of accent with the glyph lost inside it. Flutter gives the first traversable control the focus when a view takes it, so the first button on a toolbar turned into a purple square the moment anybody clicked anywhere. A foreground border is hollow, takes no pixel off the button, and is the stylesheet's outline said in Flutter.

  • Four things about the menus, and all four were the React package doing something else.

    A second menu button opens on the first press. The panel's tap-catcher was a GestureDetector, which enters the gesture arena and wins the tap — so pressing another menu button while one was open shut the panel and stopped there, and the button had to be pressed again. It hears the pointer go down and takes nothing now, which is the mousedown on the document the React package listens for.

    A ring is drawn where a keyboard put the focus, and not where a pointer did. Flutter highlights a focused control whenever the platform's highlight mode is traditional, which on a desktop it always is — so a click drew a ring, and a panel handing the focus back to its button drew one on a button nobody had touched. :focus-visible is the browser's rule and now it is this package's.

    A panel opens with the focus on the option that is already true, rather than on the first of the list. Opening the column-width menu on a document set to normal put the highlight on narrow, which is a panel pointing at an answer nobody gave.

    Every option has the glyph its counterpart has in the browser. A list of three themes with no sun, moon or half-and-half on it is a different control rather than the same one in another language.

  • A slider has a thumb, and the way back to the default is always under it. The track was a bar with no handle on it — a browser draws one on input[type=range] and a reader who has moved one is looking for it — and the reset appeared only once a value had moved, so the panel changed height the moment a slider was touched. It is present and inert at the default now, which is the rule the React package's is under.

  • Every line of a paragraph is the same height. A line box in Flutter is as tall as what is on it, so one line of Hangul and the next line of Latin inside a single paragraph were two different heights — which nothing showed until the document became selectable, and then a selection across a paragraph was a ragged stack of blocks with gaps in it rather than a run of text. A browser does not do that: line-height is the line there and a fallback font does not get a vote. A strut says the same thing here.

  • A table is as wide as what holds it, and no wider unless its columns need it. It was laid out against the width of the window — a number a renderer drawing into a pane has no business reading — so a table in one pane of split was half a screen too wide and scrolled sideways whatever was in it. width: 100% inside an overflow-x: auto is what the stylesheet says, and it is what this says now.

  • Every cell of the status bar sits on the same line. A line box is as tall as what is on it, so the size — which is never anything but digits and a unit — was drawn a little off the line the rest of the row was on, and further off in Korean, where every other cell has a Hangul word in it. The row is centred and every cell is strutted to one height.

  • A menu shuts when a pointer goes down outside it — including on another menu's button. A panel put up over a page has to hear a press it did not receive, and the catcher inside the overlay that was doing that only worked some of the time: a GestureDetector there takes the press, so the next control needs a second one, and a translucent Listener has to survive a hit test that runs through an overlay, a follower and a stack before it reaches the bottom. In a release web build it did not.

    It is a route on the pointer router now, which is the mousedown on the document the React package's menu listens for, said in Flutter. Its own button is left out, because that button is about to toggle the panel shut by itself.

  • A link is followed by a click, and not only by a tap. The selection around the document watches the mouse for a drag, and two pixels of hand movement is a drag — so on a desktop it took the gesture before the link's own recognizer could declare a tap, and clicking a link selected a word instead of going anywhere. The press is read by a Listener as well now, which is not in the gesture arena and cannot lose it; the recognizer stays, because it is what makes a link tappable to a screen reader and what answers on a touch screen. Whichever of the two gets there first follows the link, and the other stands down.

  • The mark beside the current heading is a rule and not a bracket. It was a border on the row, and a border follows that row's corner radius — so two pixels of rule came out bent into the same bracket the stylesheet was changed away from drawing a few entries above this one. Its own box, against the panel's leading edge, straight.

  • The leading is split evenly, which is what a browser does with line-height. Flutter's default divides it in proportion to the font's own ascent and descent, so where a baseline sits inside a line depends on which font drew that line — and a document is two fonts the moment it has Hangul and Latin in it. That is what left the size, the one cell of the status bar with no Hangul in it, sitting off the line the rest of the row was on. It is the browser's half-leading now, in the status bar and in every paragraph.

  • A selection in the source is a run of text and not a row of blocks. Flutter fits a highlight box to each run's own glyphs by default, so a line of Hangul and a line of Latin were highlighted at two different heights with a gap left between the lines. BoxHeightStyle.max is the browser's answer, and it is a knob a text field has. The drawn document has no such knob — Flutter paints a selection there itself, with the tight boxes hardcoded — so the gaps between lines are still there when a document is selected rather than the source.

0.1.0 - 2026-08-31 #

The first release. Everything in it is new, so each entry says what a thing is rather than what it became.

Added #

  • MawyViewer — a Markdown document, drawn and not editable. The document becomes widgets rather than a string of anything, so there is no markup on the path from Markdown to the screen: nothing to escape, and nowhere for an injection to arrive. Every widget is built on package:flutter/widgets.dart alone, so a document sits inside a Material app, a Cupertino app or a bare WidgetsApp without dragging a second design system in behind it.
  • The Markdown parser, which is the React package's parser. Not a port in spirit — ast.dart, source.dart, block.dart, inline.dart and parse.dart are ast.ts, source.ts, block.ts, inline.ts and parse.ts, function for function and rule for rule. CommonMark, with emphasis resolved by the specification's own delimiter-stack rules; GitHub's additions — tables with per-column alignment, task lists, ~~strikethrough~~, bare URLs and e-mail addresses, footnotes, and the five alert kinds; and definition lists, which GitHub does not read.
  • A check that the two parsers agree. tool/parity.dart and the React package's scripts/parity.mjs print the same trees in the same shape, over every awkward case anybody has written down plus every Markdown file in the repository, and the two are diffed. Two implementations of CommonMark drift the moment nobody is comparing them, and a document that means one thing in a browser and another in an app is the bug this library exists to not have.
  • Every node knows where it came from. A parsed node carries the range of the source it was read out of — through however many containers it was nested in, past the > a quotation puts on each line, the indent of a list item and the pipes around a table cell. Offsets are counted in the document as it was handed over rather than as the parser tidied it, so a file with Windows line endings, a byte order mark or a tab where an indent should be answers in its own characters.
  • A toolbar for how the document is set, not for what it says: typeface, text size, line height, letter spacing, column width, light or dark, an outline of the headings, and the source to the clipboard. The glyphs are Lucide's, which is what the React package draws too — the two are the same toolbar rather than two toolbars that resemble each other. toolbar takes the controls to draw and the order to draw them in, or const [] for none.
  • The palette is the React package's styles.css, value for value. A colour that is #5b34ea in a browser is #5b34ea in an app. MawyTokens.light and MawyTokens.dark are the two, and colorScheme chooses between them or follows the platform.
  • An outline panel, built from the same slugs the parser gives the headings, which scrolls the document to whichever one is chosen.
  • Korean and English chrome, through locale.

Security #

  • Every URL a document names is checked against a scheme allowlist, in the same list the React package uses. A [click](javascript:…) is drawn as the words the author wrote rather than as a link that does nothing. data: is allowed for images, and only for media types anything draws.
  • Nothing is opened. A link does nothing at all until an application says what opening one means, through onLinkTap. Opening a URL means handing it to the platform, and which URLs an application is willing to hand over is not a viewer's decision.
  • Raw HTML is shown as the characters it was written with, and there is no option to make it otherwise. Flutter has no HTML to draw it as, so there is nothing else it could be — which is why this package has no html policy to choose between.

Dependencies #

  • lucide_icons_flutter (MIT), for the toolbar's icons. It is the same icon set lucide-react draws, which is what makes the two toolbars the same toolbar, and it brings nothing else with it. It is also the one thing here that is not small: it ships its variable faces whole and Flutter's icon tree-shaking barely dents a variable font, so it is about 3 MB in a build. Ordinary in an app bundle; worth knowing about on the web.
1
likes
140
points
674
downloads

Documentation

API reference

Publisher

verified publishercdget.com

Weekly Downloads

A Markdown editor and viewer that draw the document rather than a string of HTML — their own CommonMark and GFM parser, typography the reader controls.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#markdown #editor #viewer #commonmark #gfm

License

MIT (license)

Dependencies

flutter, lucide_icons_flutter

More

Packages that depend on mawy