There are a bunch of reasons I ended up writing igk.
As a writer, I have three distinct phases in producing a book:
Writing.Editing.Typesetting.The nice thing about the LaTeX model is that it cleanly separates the first two from the third. You write in semantic markup and then there's a process that transforms this into some styled output.
The most important thing that LaTeX got (nearly) right was having consistent syntax for markup. If a chunk of text is of type foo, you write \foo{some text}. LaTeX doesn't care if foo is something built into the core, something from a package, or something bespoke to your book. And that's really important because favouring generic markup (especially favouring generic presentation markup) makes people use it. For example, in my Cocoa books, I had a custom macro for class names. This typeset them as code, but it also added them to the index under '{class name} class' and 'classes: {class name}`. If I'd used something like Markdown or AsciiDoc, the temptation would have been to use backticks ('this is code') and then going and finding them all later would have been too much effort. A lot of newer things, such as Typst, make this mistake: typing their common special-cased commands is easy, typing your custom ones is more work, and so you don't use them where you should. I am lazy, my tools need to respect that and not try to force a change in behaviour.
Importantly with an extensible semantic markup language, I can just start using new markup before I bother to define it: the writing phase doesn't need the definition, only the typesetting phase does. So I can write \thingIJustRealisedNeededToBeCustomMarkup{some text} and then go and provide the definitions later.
The problem with LaTeX is that it isn't actually a markup language. It's a stream-processing typesetter with some macros layered on top. This means that you have to be incredibly disciplined to not insert bits of TeX (or imperative LaTeX) in the middle to tweak presentation. And that matters because LaTeX is entirely geared around a print (or print-like, such as PDF) generation flow. For web and ePub, you want to preserve most of the semantic markup right to the end and then use CSS for styling.
The other aspect of LaTeX's not-quite-a-markup-language behaviour is that you have slightly odd behaviour for various things, and macros have only local knowledge, when you're lowering them to presentation markup (or to other higher-level macros) they inspect the state of the rest of the document. This is why indexes in LaTeX generate files as the document is processed then read them back and process them. The \makeindex macro can't just walk back over the document and find all of the \index nodes in the tree, because there is no tree.
Igk uses a document model that is basically XML: it's a tree of nodes. Each node contains zero or more key-value attributes and zero or more children, which are either other nodes or text. The input language is more or less the same as SILE's cleaned-up TeX syntax:
There are precisely three characters that you need to escape: \, }, and %. % is a TeX-style line comment, \ begins a command. Commands are of the form \commandName[key=value, arguments=here]{children go here}. The bit in square brackets is optional. Command names must be single identifier (alphabetical character followed by characters that are not spaces, { or [). Keys are the same kind of identifier as commands. Values are optional (treated as boolean true if omitted), values are either quoted in " (with \" to escape quotes in value names) or are whatever is there before the next comma or close bracket. If you want a literal } in the middle of a node (and most things are in the middle of a node, because it's a tree) then you will need to escape it.
That's it. It takes a few seconds to learn the syntax.
The core language doesn't define the semantics of any commands. It just builds a tree (including source locations, so you can see where every node came from in the input). It then provides an API (C++ or Lua) for passes that do tree transforms. Each transform takes a tree and produces a tree.
Even includes are handled via this mechanism. One of the passes looks for nodes whose name is 'include', treats their body as a file name, opens it, hands it to the parser, and then replaces the include node with the newly passed tree. It then runs itself over the newly parsed subtree, to catch recursive includes.
There are a bunch of passes that I've written, but if you want a completely different flow, with custom markup for your own context, that all works.
For the CHERIoT book, I added some plugins that used libclang or TreeSitter to parse source code and generate text trees. These are then used by passes that take a file name and marker name, find the begin and end markers in the file, and generate a text tree containing the numbered listing, its caption (including the file that it was taken from), and the extracted parts of the file with line numbers from the original source. All of the code listings in the book are taken from the accompanying examples and so can all be tested as actually working.
I have a few different chains of passes for producing the HTML, ePub, and PDF versions (actually, two PDF ones, one for print and one for online reading. The print one, for example, takes hyperlinks and turns them into footnotes, and uses monochrome typesetting for the code). All of these come from the same source. The PDF ones use SILE for typesetting, the rest generate [X]HTML directly from the tree. It could easily use some other typesetting engine for PDFs.