reMDer
- 30 Devlogs
- 197 Total hours
Markdown renderer/reader for the terminal! Read markdown like you would write it!
Markdown renderer/reader for the terminal! Read markdown like you would write it!
this is my last devlog
the project isn’t complete and i won’t be shipping it. you see, i was banned for 2 months for misconduct on slack (i will not provide details here but i deserved it, it was corny anyway), and as part of that ban, i can’t participate in any events, including stardance.
in this time however, i’ve done a lot. there’s now spinners for images, proper tables, and so many more improvements. keep your eyes peeled for a release on the github! 👀 also it doesn’t matter that this devlog is 15 hours over the limit, its not like i can do anything about it. also if one of you regulars in #stardance see this, can you please ask amber if i can have my sticky streak rewards? i really want the stickers and plushie 
ImageRenderable for images in rendererdiv and details elements
not very big news here, but now you can:
div and details properly! now you can click on a details element to open and close it! this didn’t even take too longTextRenderable or BoxRenderable that return a Renderable instance instead of functions like Text and Box which return a ProxiedVNode<T>. this is mostly because there’s a bunch of things a Renderable has that a ProxiedVNode doesn’tanyways im currently working on adding footnotes, so see you in a few days ![]()
details elements and opening on click/space/enterdiv and details elements
took only 10 hours, but im done with the table of contents thing! you can press shift+j to go down to the next heading, and i forgot to add shift+k
😭 anyway this took a while since i couldn’t figure out how to position the box properly and how to style it. this was fucking ass icl. i usually have much more to talk about but this wasn’t the most fun so here’s the commits:
J
we’ve got a lot of new stuff!
reMDer with a green bg and the file name next to it, but i’ll add more later!
they look really cool too!--no-render-headings or -H flag will make the headings render as normal text with colors instead of figlet fonts now!q to quit, ? to toggle the help menu, and C (shift+c) to open the console in debug mode and that’s about ithere’s what i’m gonna do next:
leaf markdown viewer
we’ve got loads of new cool stuff that get rendered now!
first, github alerts/callouts now render nicely! they look like this:
> [!TAG]
> content
where TAG can be NOTE, IMPORTANT, TIP, WARNING or CAUTION. they also have these cool icons next to them, but the catch is you need a nerd font for it. i’ll add an option to not need it and only use ascii characters
next, we have highlighted code blocks! the renderer can highlight hundreds of languages using shiki! you might not even have heard of some of the languages that it can highlight! imo its really nice looking and i love it!
the bulk of this time came from adding support for task list items because i couldn’t figure out how to do it. eventually after some tinkering i’ve got it to work! it looks kinda cool too tbh
i also got emoji support using an extension. it automatically converts :emoji: to the corresponding emoji if its in the unicode specification.
finally, i added a simple height option that just sets the height of the renderer. it allows you to set the height of the renderer manually and that’s about it lmao
while this is quite out of schedule (i usually post every 2-3 days), today brings some peak stuff! the peakest part is at the end
firstly, i have in fact fixed tables! the issue was i was pushing the accumulated cells array to the rows array in the wrong loop. i was doing this in the loop that iterates over the cells in a row, so it was causing it to repeatedly push the same strings over and over. the fix was putting it in the loop just outside, that is, the loop that iterates over the rows in the table token!
i’ve also improved the image handling! now Image classes have the buffer property renamed to content, and has a new property called properties, which is always {}. with this, it has all the properties of ProcessedToken, so my lsp won’t complain that some property doesn’t exist on Image
speaking of ProcessedToken, its now an interface instead of a type. there’s also a new TableToken interface that extends ProcessedToken, with an alignment property. this makes it a bit easier to handle tables since the content property is narrowed. also, it seems like opentui’s tables don’t support alignment, so i might have to make a custom table function.
i also upgraded opentui from 0.5.7 to 0.5.8! i have no idea what changed, but whatever 
i’ve also stopped using chalk to color my errors since the opentui console doesn’t support ansi 
now the peakest part is you can pipe markdown into the program. this is pretty peak and you must agree since there’s a lot of stuff you can do with this. for example, you could render arbitrary markdown without having to create a file first!
it has a limitation though: if you’re on windows, you can’t use the interactive mode, and the rendered content is forced to print to stdout. this is because of a bug with bun. if you’re piping into the program, it’ll get the input by calling await Bun.stdin.text(), but this stops sending keypresses to opentui because of a bug in the bun runtime, so you can’t scroll down. i solved this issue (or rather, chatgpt solved this issue) by opening a read stream on /dev/tty. the issue with this is that /dev/tty doesn’t exist on windows since its not unix-like. my idea was to just, not let you do interactive mode on windows if you’re piping content to it. again, this is a bug with bun, and i’ll remove this limitation once one of the two is merged. that’s right, there’s two! here’s one for reference: https://github.com/oven-sh/bun/pull/32153
shit that was long 
renderer.ts from stdinpls like 
from my last devlog:
next up:
- code blocks (~2-4 hours)
- fixes to table rendering (<30 mins)
- fix image handling (it doesn’t handle relative paths correctly) (<1hr?)
of these, i’ve done none!
what i have done though is add flags and types! i spent a while dealing with type errors because i decided to specify types instead of just putting any on ProcessedToken.content. i also added a debug flag, but literally all it does is show the console 
i also fixed the width thing, so the content is only as wide as the space you give it
so cooll!! ![]()
the main part of this is that i added a --print-to-stdout flag that just prints the formatted markdown to stdout! you can just scroll in your terminal instead of having a whole ass tui
its kinda borked and incomplete though, so i’ll fix it up! it does print some garbage at the beginning so that’s not great
anyways, i’d better do what i said i’ll do last devlog! i went off track for this lmao
if you’ve read my last devlog, you might remember that i was considering the jsvision library. however, i came back to opentui because its still much simpler. also, i found that there are libs to adapt ansi! or at least chatgpt did
at first i was doing ghostty-opentui, but i realized after some time that it was literally a whole terminal. then i got chatgpt to find me this package called ansi-sequence-parser (https://www.npmjs.com/package/ansi-sequence-parser) and its perfect! so now im using that to render the bold and italics! tbh i could just eliminate the middle stage (stylize.ts), but i put a lot of work into it and i’m extremely vulnerable to the sunken cost fallacy, so i’m doing this bloat lmao.
also, i got ordered lists, bullet lists, and blockquotes working! they look so cool!
as for the menu, its not doing very fine, but that’s because i’ve not worked on it! i’ll get to it, don’t worry!
next up:
As a prize for your great work, look out for a bonus prize in the mail :)
well i’ve messed up big time, sunken cost fallacy is real and it got me
you see, i was using opentui earlier, but i decided to change because i realized that it doesn’t support ansi escape sequences. i landed on neo-neo-blessed, and it seemed cool, until i realized the list selection thing got fucked up because it didn’t handle scrolling list options with heights greater than 1 row properly which is a deal breaker. i also spent a lot of time patching its types, in fact that’s what most of this time went to 
i think i’ll use jsvision. it seems interesting and i’ve got nothing to lose. the only issue is it has one maintainer and 9 stars, but gpt found it, and it seems cool. also they have demos of the unmodified lib running in the browser which i wanted to do, so this will be fun ![]()
i’ve got news: the TUI renders!
for this tui, im using opentui! right now, the tui only shows an interactive list of files, and its supposed to render the file content if you select one, but that’s not happening right now. i’ve been working on the function to make opentui render the markdown. while opentui does have its own thing for markdown rendering (
), i’m gonna render my own markdown. im currently trying to get tables working!
holy shit it works, the problem was that i was running a function but i wasn’t putting it in a console.log. the test function for reference returned a string. also, unknown tags are handled somewhat properly. i need to actually fix that, but once i do, im gonna start on the tui!! its been 96+ hours now
what a crazy thing, i expected this to take 10-20 hours and now look at this…
anyways, i’ll go fix the unknown tag thing! after that i’ll start working on the tui 
well we’ve got some nice stuff now! here’s a list of everything, including problems!:
mark elements are now supported, which just put a yellow bg on some textblock property
i’ve moved to typescript, cleaned up some code, finished that image function from last devlog, and upgraded packages!
bun repl i have to do this
this was a lot of work, and i’m still not done with html in the stylizer! here’s a list of changes with explanations:
aliases array in the html parsing section: this makes it so that html tokens and markdown tokens have the same typesset: sets have a .has() function that lets us find an element in O(1) time complexity which is much faster compared to Array.prototype.includes() which has an O(n) time complexityproperties keyundefined or whatever, it will catch it, and throw a more useful error with better debug infotoken.level and token.nesting properly when processing html: this is necessary because the while loop depends on the nesting to know when to stop accumulating tokens and process them_open in their .type property_open get handledelse if(...) {..})
Image that has a function to render the image. its nearly complete but it has some errors that i committed because i need to commit often lmaonow here i used chatgpt a lot for debugging. here’s exactly what i used it for:
level property of the tokenstoken.level assignmentyou may have noticed i’ve been going more and more time without devlogging. that’s because i like devlogging when there’s a big new thing, not for every small little thing. my plan for this devlog was to announce that i was done with processing html in the stylize function, but that hasn’t happened yet because well, there were a latta bugs here and there, and i had to fix those too. as for the length of the devlog text, i like writing about my progress here because its the only place where it makes sense to talk about it in-depth
HTML parsing is now complete
its been nearly 2 weeks and i have great news: html parsing is complete
what this means is that html blocks get processed into token streams like the one’s markdown-it makes. now, i must clarify that these html blocks were already in a token stream but they were distinctly html, having the type html_block, and if i didn’t convert those html blocks into markdown-it’s _open/_close tokens, i’d have to parse it in the file that styles the tokens, and many html tags do the exact same thing as their markdown counterparts so i could reuse some functions. i must also clarify that i have NOT done anything for stylizing html in the stylize.js file, which deals with processing text formatting and image processing
next up: stylizing html!
this is way earlier than my normal schedule, but that’s because i’ve got something to show. basically, inline rendering was bugged, and not only did i fix it, i also made it so that it works better! that’s all you need to know, do NOT read the rest of this if you don’t want to be bored to death, its a really lengthy explanation of this
the initial bug was that inline rendering wouldn’t work at all. i figured out the problem was that i wasn’t pushing "inline" to the state array somewhere. i realized that it wasn’t working when called in the table rendering function, and that was because i wasn’t pushing "inline" to the state array.
the reason for this is because the renderInline function slices the state array at the index after the "inline" string, so if its something like ["paragraph", "inline", "bold", "italic"], the function slices at index 2 because "inline" is at index 1, and we get ["bold", "italic"]. the problem is, if "inline" doesn’t exist, running indexOf("inline") returns -1, so it slices at -1 + 1, which is 0, so its just the whole array. but anything before “inline” isn’t a valid style, and since the function for that is undefined, it goes kaboom and throws an error saying that it’s not a function.
also gpt found 5 other bugs but they’re kinda irrelevant
the fix was making it so that the renderInline function itself pushed "inline' to the state array instead of depending on it being already there. this also removed the need for making sure that the inline string was already there. i might do this with the rest of the functions too actually :goonology: also removed unnecessary state.pop() occurrences since the pop method is destructive and can mess up everything
as for the improvement, you might see in the first screenshot that there’s a bunch of text tokens together. yk, they could be squished together into one token… oh right, that’s what i did! i changed the renderInline function to make a variable where the text accumulates until there’s no more text or if there’s an image, on which it gets pushed to the contents array
now the most important thing is that there’s only one more thing left to do: html rendering. this is the last thing before i get to actually rendering stuff on the screen! this is exciting!

please like this took more than an hour to write

in short, tables now work and the inline rendering function is fixed! this took a while because i genuinely had no idea what i was doing. now the long stuff:
i got distracted while writing this and realized everything’s actually borked, but tables do somewhat work! its still borked tho–
also i realized that my whole code is borked, but…
still have no idea how i missed that
i just changed image function so that it returns an object with a prototype function instead of unnecessarily repeating the function as a property of all the image objects, so if there’s 10 pictures in a file, there will only be one function to render each as opposed to 10 instances of that function 
oh fuck, my code is COMPLETELY borked

gng TS IS PEAK!!!!
i added a function that renders code AND ITS PEAK JUST SEE THE SS
now some of this time came from trying to figure out why my code was joining the contents of a function, and it was because i forgot to add the argument that gave the coloring function (chalk library) the text to color. hey, at least i found a way to read the contents of functions
time to go steal code from closed-source packages
leaving that aside, my next step is thematic breaks. this shouldn’t be too hard, in fact, i’ll do it rn since it’s that easy. rn its 8:49 PM GMT+0300, i’ll tell in the next paragraph when i finish it.
[8:54 PM] just did it, it only took like 2 mins, the other 3 were because i found a bug that never removed codeBlock from the stack after it was added.
that’s about it, and this devlog was quite short. well, comparatively, to my other devlogs, after all there’s people posting 4 word devlogs. you should follow this project tho 
when the FUCK did this become 4 hours 
so uhh i spent like all of this time debugging and improving my code.
first thing is, i replaced these “microfunctions”, as i call them, with
an object that had keys whose values were arrow functions. these arrow
functions did the exact same thing and took up less space and was even
more readable!
second, i spent some time on a stupid bug where the main function in
stylize.js would hang. i thought it was an issue with the image
function, but after half an hour or so, when i added console.log(done)
right before the return, i realized it had to do with the main function
that was calling the image function, and sure enough, the second i
looked at it, i realized the problem: it was a while loop and i forgot
to add a line that incremented the index, so it just kept doing the
same thing forever until i killed the process
oh well, more hours for me 
third, i improved (or rather cleaned) my image function. it returns
promises and doesn’t use terminal-image directly. instead, i made it
so that it would return an object with a bunch of keys, one of which is a
function that takes the whole object as an input. this function
actually renders the image, since i plan to write the function to
display it in a way that deals with image rendering after rendering
text, and while the images get rendered, a spinner takes their place
temporarily, but the images replace those spinners once they are
processed
fourth thing is, i made it so that the main function calls the
renderInline function instead of the paragraph function. in fact,
this paragraph function is completely useless, so i went ahead and
removed it too.
lastly, i upgraded a bunch of packages:
typescript from version 5 to 5.9.3got from version 15.0.7 to 15.1.0markdown-it from version 14.2.0 to 14.3.0shiki from version 4.3.0 to 4.3.1if you wonder what these packages are
typescript is, well, the typescript language, not sure why i havegot is some package that does pretty much the same thing as thefetch() but it can give me a buffer, which i need for imagesmarkdown-it is the markdown parsershiki is for coloring code. fancy, right?aight this took me 2 hours to write and i started coding again in the
middle of writing this 
oof! that was a lot of work, but now we’re REALLY getting somewhere!!!
first thing, headings actually work now, and if there’s a link in the
heading, it gets displayed right below the heading! tbf i did rip the
heading thing straight out of the original index.js file when i was
trying to make a markdown parser using regex 
secondly, and the more important thing, image rendering now works. pretty big milestone. i achieved this through the terminal-image npm package. however, this took a while because i couldn’t figure out why it wouldn’t render. the problem was tmux–it was stripping the codes it didn’t recognise
so i implemented another system to detect if the terminal had color and/or image support to decide whether to use ansi blocks or actual images. now, it works! ts so peak fr
index.js)heyo! this is a long one, so i’ll go over this commit by commit
(0ad2cb2) Start paragraph parsing
here, i did these:
state to maintain the nesting while styling. managing this was quite tedious and uselessgetStyle(). i’ll explain the use for it below.heading() and paragraph() functions as well as the stylize() function(eb218d2) Add markdown extensions
as for this one, i added a bunch of packages, mostly extensions for markdown-it. these are:
markdown-it-footnotemarkdown-it-github-alertsmarkdown-it-sanitizermarkdown-it-tablemarkdown-it-task-listsshiki (for code highlighting)terminal-imagebesides these, i also configured the plugin to use the plugins, enable the autolink extension and also replace all HTML line breaks (<br>) with double newlines (\n\n)
(5ecef2c) Start working on image rendering and colors/styles
now here is where i found a gem in my dependency tree. you see, i was searching for a package to handle color rendering since i sure as hell wasn’t gonna deal with the hex codes shiki, the code-highlighting plugin, was giving me for the colors of the tokens. so i saw a package called chalk and it sounded familiar. turns out, shiki had chalk as a dependency, so it was already installed!
for that reason, i added a chalk import in this commit. i also started working on an image function to render images, and named it renderImage(). also added a great error message if two requirements weren’t met:
throw new Error("WRONG TOKEN IDIOT DEV");
but that was all i really did in that function
(0dc393d) Remove unnecessary object processing and dependencies
this is where i removed markdown-it-footnote and markdown-it-table because i realized there just wasn’t a point in those since they weren’t letting me test my function, they were erroring out for some reason and i found out markdown-it-table already had support for markdown tables. i also removed the mapping function in the parser stage that i made to give myself only a few fields. it was pretty useless since i kept randomly finding that i’d need some new property.
(8a5200c) Continue work on inline parsing, add terminal-link
latest commit, this is where i did a lot of work:
terminal-image as a dependency since i found that bun already provided a class for it (Bun.Image) and it also had webp support unlike terminal-image, so no point in another package when a built-in one works better
getStyles() function betterbold(), italic(), etc. i also renamed renderImage() to image()
image() function because why 2 if statements when 1renderInline() function. now it uses computed function names that call those micro-functions.state to a global array, made things much easier.heading() and paragraph() functions furtherthat’s about it gng, also its 12:28 am for me rn
this is why we devlog more often gng ✌️
toiling away at this code…
today was a bunch of stuff
renderInline() function to render stuff inline. i still need to add images and links, but i’d need a lib for links, and maybe for the images tooheading() function that will render headings, emphasis on will because its not being called yeti revamped the parse-input.js file! now, it has two functions:
giveFields(obj)
this is a function called by the second function. since markdown-it
(the parser im using now!) has a lot of fields that i don’t need. so
this function returns a mapped array with the fields type, tag,
nesting, content, and children. as for this children field, the
children field is an array of objects, where the objects are just like
the parent object, with lots of unnecessary fields. for those, i used
recursion! (it’s my first time using recursion 
)
parse(input):
this is the function that i’ll be using to get the tokens i need to
style! what it does is pretty simple:
MarkdownIt constructor (? i’m notmarkdown-it’s tokens using their parsing functiongiveFields() if your memory is that terrible). againmarkdown-itin other news, i wrote 2 more test markdown files. one tests the
behaviour of headings and the other tests the behaviour of inline stuff like bold and italic text!
fuck this rewrite, i’m abandoning this shithole of a parser i’m trying
to make, i’ll just go use an already existing parser, its just not worth it
it’s too much hassle to make an in-house parser, so here’s my plan:
i started a rewrite! what i was doing earlier was honestly too messy to actually use.
my earlier way was making a bunch of regexes and using those to match the remaining text. my new method is still using regexes, but i split the whole thing into two parts: block parser and inline parser.
i added a backslash to the giant block letters i made for headings!
(bac8c5a) Start parser rewrite
(664713e) Add backslash in heading
help me what on god’s green earth is this 
so after dealing with a shit ton of regex and doing pretty much nothing with it besides some stuff that would improve performance, i have decided that regex is not the way. instead, i’ll just set a bunch of flags with empty content and make a separate function that processes everything. i think the styling function will handle tokens because that’s its job, to style the tokens. at least i have something to follow now. (i did not implement any of this yet)
fc8de72: Fix some regexes
fe6eeef: Change some regexes
f67c3b5: Change matching system in parser
8a80409: Add some HTML support in lexical parser
0727d82: Finish lexical parser (almost)
b7bc45b: Make lexical parser work
i got bombed by my own regex
anyways, so i started making a lexical parser and uhh its not going all sunshine and rainbows. the blockquote regex doesn’t really work right and also i checked the gfm spec since i want to adhere to it and uhh fuck i might be cooked. i thought ts was going to be a 20h project help meeeeeee
also yes it took me 6 hours for 74 lines of code sob this is just great
took only this long to make a bunch of letters
you see, i had to make a bunch of custom glyphs to render headings in the terminal. making that for pretty much all the characters on your keyboard isn’t quite easy but hey its done now. the thing is, this is only for level one headings. once i finish the actual parser, i’ll add glyphs for the rest of the headings. in the meantime, i’ll have to do the rest. i did not expect to have spent this long on the project already yk
a terminal markdown reader that has actual headings? never could have thought of it huh
well this is what i’m doing. literally a markdown reader that prints text the way you might have written it.
anyways so this is gonna use bun since bun is ultra fast. also this might be a 10-20h proj, shouldn’t be too hard.
well i like more hours, so i might also add stuff like:
well i don’t really know what else to put so ig that’s it? anyways i have actually done some work, that is, my file currently ouputs some text. literally the raw original text. what’d you expect from 24m of coding