Vote Charlie!

Flashcard text finally automatically sizes

Posted at age 29.
Edited .

Over the past few months of using Anki consistently, I have been trying to streamline my process of adding and organizing cards, as well as making them somewhat pretty. It has not been easy!

anki-code-editor.png

anki-code-editor.png

Ideally I would detail specific bugs with code and screenshots so I could help the project itself, but for now this entry will serve more as a personal journal entry giving some idea what I have been trying to do.

Making text fill card

I assumed it would be simple to make my content fill the card. Anki does not support external JavaScript, so that precludes using any libraries. Even with CSS3’s new options such as viewport units, I haven’t come up with a CSS only way to make card text fill the card. This is vastly complicated by the fields and even HTML structure differing depending on the card and the deck, and I am trying to cram as much as I can into a single card type to make maintenance easier. My approach was to “simply” scale the body font size till the point it caused a scrollbar, and then back off a bit. In practice, this was not working at all because my scrollHeight was frequently equalling or being less than window.innerHeight, but there was actually a scrollbar. In researching other approaches, I somehow did not find anyone trying to do the same thing.

Yesterday, I figured out a possible culprit was embedded fonts, missing fonts, or possibly a specific font, as described below. I already spent more time than I wanted “precrastinating” before doing my flashcards which themselves are precrastination when I should be focusing on the job search. Why does everything I touch interest me to the point of sending me down a rabbit hole?!

For reference, I’ll include my hacked together JavaScript here. Doing proper development has been tough since Anki seems to use a different parser for the screen where you edit code and the actual flashcard studying interface. There’s also a preview window in the card browser, and I haven’t decided if this is itself a third implementation. There’s no console, and until yesterday, I believed alert() did not work due to reading the manual, but now I see it does work. What I did previously was output the entire page onto the page after running through JavaScript’s encodeURIComponent() function, then decoding it and working with it in a browser. But in practice, most of my “development” has been in tiny increments using Anki’s terrible code editing interface while I am studying my flashcards and get driven to fix something. Sometimes I even edited the CSS and JavaScript on Android. Not easy!

Then, the JavaScript only works at all because I put it in Anki’s style box. Since I observed this gets transplanted between <style> tags, I figured out I could close the tag, open a script tag, close that and reopen a style tag all in the “styles” box, and get JS to parse. It also only worked when I defined the functions there but actually called them from the card HTML. I did not explore the parse order more indepth beyond observing I was not able to circumvent Anki’s handling of embedded audio files.

Once I figure out what my long term workflow and card structure will be, I will make another pass at all this. (Note the name currentDeckMessages no longer reflects it’s purpose.)

@import url("_dark4.css");
</style><script>
var body = document.getElementsByTagName("body")[0];
// body.insertAdjacentHTML("beforeend", '<div style="white-space:pre;position:fixed;top:0;left:0;width:100%;height:100%;z-index:999;text-align:left;font-size:12px;color:#fff;background-color: rgba(0,0,0,0.50);font-family:Monaco,monospace;" id="debug">DEBUG</div>');
var printDimensions = function(pre, extra){
    if( ! document.getElementById("debug") ) return;
    document.getElementById("debug").insertAdjacentHTML("beforeend", "<br>" + pre
        + " W:" + window.innerWidth + "x" + window.innerHeight
        + " O:" + body.offsetWidth + "x" + body.offsetHeight
        + " C:" + body.clientWidth + "x" + body.clientHeight
        + " S:" + body.scrollWidth + "x" + body.scrollHeight
        + extra
    );
}

function setStyle(selector, propName, propVal) { [].forEach.call(document.querySelectorAll(selector), function (el) { el.style[propName] = propVal; }); }

function currentDeckMessages() {

    var article = document.getElementsByTagName("article")[0];
    setStyle('.subdeck', 'display', 'none');
    var subdeck = article.dataset.deck.replace(/^General(?:::Language)?::((?:(?!::).)*)(?:::.*)?/, '$1');
    if(subdeck === ''){ setStyle('.subdeck.Other', 'display', ''); }
    else { setStyle('.subdeck.' + subdeck, 'display', ''); }

    var section = document.getElementsByTagName("section");
    for(var i = 0; i < section.length; i++){
        // replace hyphens not inside tags (properties) with nonbreaking hyphens
        section[i].innerHTML = section[i].innerHTML.replace(/-(?![^<]*>|[^<>]*<\/)/g, '\u2011');
    }

    var body = document.getElementsByTagName("body")[0];
    var article = document.getElementsByTagName("article")[0];

    // var bodyFontSize = parseInt(document.defaultView.getComputedStyle(body).getPropertyValue('font-size'));
    var bodyFontSizeMin = 25;
    var bodyFontSizeMax = 725;
    var bodyFontSize = bodyFontSizeMin;

    function setBodyFontSize(int){ body.style.fontSize = int + 'px'; }

    var step = 350;
    // while( step >= 1 ){
    while( step >= 0.1 ){
        do {
            bodyFontSize += step;
            setBodyFontSize(bodyFontSize);
            printDimensions("i ", " fS:" + bodyFontSize + " step:" + step);
        } while( window.innerWidth >= body.scrollWidth && window.innerHeight >= body.scrollHeight && bodyFontSize <= bodyFontSizeMax && bodyFontSize >= bodyFontSizeMin );
        bodyFontSize -= step+2;
        if( bodyFontSize < bodyFontSizeMin ){ bodyFontSize = bodyFontSizeMin; }
        // step = Math.floor(step / 4);
        step = step / 4;
        setBodyFontSize(bodyFontSizeMin);
        setBodyFontSize(bodyFontSize);
        printDimensions("o ", " fS:" + bodyFontSize);
    }

}

function playSound() {
    var a = new Audio(document.getElementById("audio"));
    a.play();
}

</script><style>

Embedded fonts

I had been doing pretty well using custom fonts that worked both on the macOS Anki as well as AnkiDroid, despite the manual stating:

Embedded fonts currently do not work on OS X. It is still possible to use custom fonts, but they need to be installed system wide.

My strategy was to reference the fonts by their real names, but include a source URL. I am pretty sure even bold worked on my Android, but perhaps it was emulated by the browser, as I did not previously use a real bold font file. My card styling included this:

@import url("_dark4.css");

Then I put _dark4.css in my media folder, and it started with these lines:

@font-face { font-family: "Optima Regular"; src: url('_Optima-Regular.ttf'); }
@font-face { font-family: "Monaco"; src: url('_Monaco.ttf'); }

I started investigating this more yesterday when I finally tracked down my JavaScript resizing issues to be caused by the font Monaco. I discovered the file had gone missing from my Anki media folder, but since it was a local font on my Mac, it loaded. Something about the external reference to a missing file caused a delay that screwed with my (admittedly very unrefined) body.scrollHeight detection. When I replaced the missing file, the issue persisted, but I lost confidence in what was happening since debugging JS in Anki is a pain! Eventually I decided to eliminate doubt by using a fake font-family name so I could be confident the external font file was being used.

@font-face { font-family: "text_font"; src: url('_Optima-Regular.ttf'); }
@font-face { font-family: "code_font"; src: url('_InputMonoNarrow-Medium.ttf'); }

Upon changing the previous Optima Regular and Monaco references to text_font and code_font, my JS resizing worked almost perfectly. I still had a minor issue where a scrollbar would sometimes appear, but I think this is unrelated. Since scrollbars make the window narrower, you must reduce the font size beyond the size that initially caused the scrollbars in order to make them disappear. I worked around this by backing off further in each loop, and it seemed to work. This last issue I resolved by reducing the font size a bit after the final calculation, but it’d be nice to pin down the issue. Anyway…

Once I was pretty happy with the resizing, I realized bold and italic were not working. This made sense since I was not referencing bold or italic font files. I thought it would be a simple matter of adding the appropriate files to my media folder and modifying the CSS as follows (and I switched from Monaco to Input Mono):

@font-face { font-family: "text_font"; src: url('_Optima-Regular.ttf'); }
@font-face { font-family: "text_font"; src: url('_Optima-Bold.ttf'); font-weight: 700; }
@font-face { font-family: "text_font"; src: url('_Optima-ExtraBlack.ttf'); font-weight: 900; }
@font-face { font-family: "text_font"; src: url('_Optima-BoldItalic.ttf'); font-weight: 700; font-style: italic; }
@font-face { font-family: "text_font"; src: url('_Optima-Italic.ttf'); font-style: italic; }
@font-face { font-family: "code_font"; src: url('_InputMonoNarrow-Medium.ttf'); }

I can’t currently figure out how to consistently use the body or italic fonts in my cards, though. There might be bugs standing in my way, but I did not exhaustively test. I got bold to display sometimes, but it didn’t make a lot of sense. If I put the full font-family: "text_font_bold"; src: url('_Optima-Bold.ttf'); font-weight: 700; in a style attribute on a tag, it worked. If I reduced that to style="font-family:text_font_bold;font-weight: 700;", it did not. If I misspelled the text_font_bold, it suddenly worked. But if I removed text_font_bold leaving style="font-family:;font-weight: 700;" it did not.

I thought I could work around the issue by using a separate font name for each style:

@font-face { font-family: "text_font"; src: url('_Optima-Regular.ttf'); }
@font-face { font-family: "text_font_bold"; src: url('_Optima-Bold.ttf'); font-weight: 700; }
@font-face { font-family: "text_font_black"; src: url('_Optima-ExtraBlack.ttf'); font-weight: 900; }
@font-face { font-family: "text_font_bolditalic"; src: url('_Optima-BoldItalic.ttf'); font-weight: 700; font-style: italic; }
@font-face { font-family: "text_font_italic"; src: url('_Optima-Italic.ttf'); font-style: italic; }
@font-face { font-family: "code_font"; src: url('_InputMonoNarrow-Medium.ttf'); }

But it is still not working. Maybe the Anki manual was correct in the external fonts not working on macOS, but I had them working pretty well before! I’m going to try using the real font names again and see if I can get it to work without causing the scrollHeight being wrong issue.