ryanpcmcquen / workflowyCodeFormatter

:hourglass_flowing_sand: A simple and lightweight solution to the WorkFlowy code dilemma. | https://addons.mozilla.org/en-US/firefox/addon/workflowy-code-formatter/ | https://chrome.google.com/webstore/detail/workflowy-code-formatter/kglihipcanlbglgikjghocmbbbbkfemn
Mozilla Public License 2.0
39 stars 6 forks source link

Extension causing data loss #18

Closed curiousepic closed 4 years ago

curiousepic commented 4 years ago

According to Workflowy, this extension is causing data loss. I've used the extension for years, but this is the first occurance of this issue, so may be due to the July 24 update. https://twitter.com/WorkFlowy/status/1290372459837808641

Screen Shot 2020-08-03 at 3 37 56 PM
ryanpcmcquen commented 4 years ago

@curiousepic, I am very sorry! Can you verify which version you are using? There was a known issue with data loss due to extension stores forcing this extension to do html sanitation, but I refactored the entire plugin to fix that issue. This should not be happening on 0.9.1.

curiousepic commented 4 years ago

0.9.1 is the version I have currently installed. It's possible that it only updated to this version when I relaunched chrome, but before closing chrome I checked that the last update date of the extension was July 24. I've disabled it but not uninstalled it. Thankfully, the data was still present (cached?) on my iphone app, and I edited all the items there to update them and hopefully keep them once I reload the website, though the first time this happened (thankfully with fewer items), the iphone app had already pulled the blank versions, so the loss does seem real.

ryanpcmcquen commented 4 years ago

@curiousepic, if you are willing, would you mind testing the extension again? If data loss is still happening I will remove the extension until I can fix the bug. And I am very sorry you hit this issue.

curiousepic commented 4 years ago

@ryanpcmcquen I will give it a try with some dummy data. Considering it only affected what was viewed, I'm hoping it will be safe.

curiousepic commented 4 years ago

@ryanpcmcquen Bad news; easily reproed

Screen Shot 2020-08-03 at 4 10 29 PM Screen Shot 2020-08-03 at 4 11 45 PM
ryanpcmcquen commented 4 years ago

@curiousepic, I am so sorry. Thank you for reporting this. I'm working on a fix right now. If I don't have something today I will unpublish the extension.

curiousepic commented 4 years ago

@ryanpcmcquen Interestingly, after disabled the extension and reloading workflowy, those items were in fact restored, though some of my real items lost in the initial occurance do seem to be gone 🤔

ryanpcmcquen commented 4 years ago

@curiousepic, I think it's actually more of a display issue than anything, but yeah, it sucks.

ryanpcmcquen commented 4 years ago

@curiousepic, will you try pasting this in the console and see if you still have data loss:

/*! codeFormatter v6.1.1 by ryanpcmcquen */
//
// Ryan McQuen

/**
 * @fileoverview microlight - syntax highlightning library
 * @version 0.0.7
 *
 * @license MIT, see http://github.com/asvd/microlight
 * @copyright 2016 asvd <heliosframework@gmail.com>
 *
 * Code structure aims at minimizing the compressed library size
 */

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['exports'], factory);
    } else if (typeof exports !== 'undefined') {
        factory(exports);
    } else {
        factory((root.microlight = {}));
    }
})(this, function (exports) {
    // for better compression
    var _window = window,
        _document = document,
        appendChild = 'appendChild',
        test = 'test',
        // style and color templates
        textShadow = ';text-shadow:',
        opacity = 'opacity:.',
        _0px_0px = ' 0px 0px ',
        _3px_0px_5 = '3px 0px 5',
        brace = ')',
        i,
        microlighted,
        el; // current microlighted element to run through

    var reset = function (cls) {
        // nodes to highlight
        microlighted = _document.getElementsByClassName(cls || 'microlight');

        for (i = 0; (el = microlighted[i++]); ) {
            var text = el.textContent,
                pos = 0, // current position
                next1 = text[0], // next character
                chr = 1, // current character
                prev1, // previous character
                prev2, // the one before the previous
                token = (el.innerHTML = ''), // current token content // (and cleaning the node)
                // current token type:
                //  0: anything else (whitespaces / newlines)
                //  1: operator or brace
                //  2: closing braces (after which '/' is division not regex)
                //  3: (key)word
                //  4: regex
                //  5: string starting with "
                //  6: string starting with '
                //  7: xml comment  <!-- -->
                //  8: multiline comment /* */
                //  9: single-line comment starting with two slashes //
                // 10: single-line comment starting with hash #
                tokenType = 0,
                // kept to determine between regex and division
                lastTokenType,
                // flag determining if token is multi-character
                multichar,
                node,
                // calculating the colors for the style templates
                colorArr = /(\d*\, \d*\, \d*)(, ([.\d]*))?/g.exec(
                    _window.getComputedStyle(el).color
                ),
                pxColor = 'px rgba(' + colorArr[1] + ',',
                alpha = colorArr[3] || 1;

            // running through characters and highlighting
            while (
                ((prev2 = prev1),
                // escaping if needed (with except for comments)
                // pervious character will not be therefore
                // recognized as a token finalize condition
                (prev1 = tokenType < 7 && prev1 == '\\' ? 1 : chr))
            ) {
                chr = next1;
                next1 = text[++pos];
                multichar = token.length > 1;

                // checking if current token should be finalized
                if (
                    !chr || // end of content
                    // types 9-10 (single-line comments) end with a
                    // newline
                    (tokenType > 8 && chr == '\n') ||
                    [
                        // finalize conditions for other token types
                        // 0: whitespaces
                        /\S/[test](chr), // merged together
                        // 1: operators
                        1, // consist of a single character
                        // 2: braces
                        1, // consist of a single character
                        // 3: (key)word
                        !/[$\w]/[test](chr),
                        // 4: regex
                        (prev1 == '/' || prev1 == '\n') && multichar,
                        // 5: string with "
                        prev1 == '"' && multichar,
                        // 6: string with '
                        prev1 == "'" && multichar,
                        // 7: xml comment
                        text[pos - 4] + prev2 + prev1 == '-->',
                        // 8: multiline comment
                        prev2 + prev1 == '*/',
                    ][tokenType]
                ) {
                    // appending the token to the result
                    if (token) {
                        // remapping token type into style
                        // (some types are highlighted similarly)
                        el[appendChild](
                            (node = _document.createElement('span'))
                        ).setAttribute(
                            'style',
                            [
                                // 0: not formatted
                                '',
                                // 1: keywords
                                textShadow +
                                    _0px_0px +
                                    9 +
                                    pxColor +
                                    alpha * 0.7 +
                                    '),' +
                                    _0px_0px +
                                    2 +
                                    pxColor +
                                    alpha * 0.4 +
                                    brace,
                                // 2: punctuation
                                opacity +
                                    6 +
                                    textShadow +
                                    _0px_0px +
                                    7 +
                                    pxColor +
                                    alpha / 4 +
                                    '),' +
                                    _0px_0px +
                                    3 +
                                    pxColor +
                                    alpha / 4 +
                                    brace,
                                // 3: strings and regexps
                                opacity +
                                    7 +
                                    textShadow +
                                    _3px_0px_5 +
                                    pxColor +
                                    alpha / 5 +
                                    '),-' +
                                    _3px_0px_5 +
                                    pxColor +
                                    alpha / 5 +
                                    brace,
                                // 4: comments
                                'font-style:italic;' +
                                    opacity +
                                    5 +
                                    textShadow +
                                    _3px_0px_5 +
                                    pxColor +
                                    alpha / 4 +
                                    '),-' +
                                    _3px_0px_5 +
                                    pxColor +
                                    alpha / 4 +
                                    brace,
                            ][
                                // not formatted
                                !tokenType
                                    ? 0
                                    : // punctuation
                                    tokenType < 3
                                    ? 2
                                    : // comments
                                    tokenType > 6
                                    ? 4
                                    : // regex and strings
                                    tokenType > 3
                                    ? 3
                                    : // otherwise tokenType == 3, (key)word
                                      // (1 if regexp matches, 0 otherwise)
                                      +/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/[
                                          test
                                      ](token)
                            ]
                        );

                        node[appendChild](_document.createTextNode(token));
                    }

                    // saving the previous token type
                    // (skipping whitespaces and comments)
                    lastTokenType =
                        tokenType && tokenType < 7 ? tokenType : lastTokenType;

                    // initializing a new token
                    token = '';

                    // determining the new token type (going up the
                    // list until matching a token type start
                    // condition)
                    tokenType = 11;
                    while (
                        ![
                            1, //  0: whitespace
                            //  1: operator or braces
                            /[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr),
                            /[\])]/[test](chr), //  2: closing brace
                            /[$\w]/[test](chr), //  3: (key)word
                            chr == '/' && //  4: regex
                                // previous token was an
                                // opening brace or an
                                // operator (otherwise
                                // division, not a regex)
                                lastTokenType < 2 &&
                                // workaround for xml
                                // closing tags
                                prev1 != '<',
                            chr == '"', //  5: string with "
                            chr == "'", //  6: string with '
                            //  7: xml comment
                            chr + next1 + text[pos + 1] + text[pos + 2] ==
                                '<!--',
                            chr + next1 == '/*', //  8: multiline comment
                            chr + next1 == '//', //  9: single-line comment
                            chr == '#', // 10: hash-style comment
                        ][--tokenType]
                    );
                }

                token += chr;
            }
        }
    };

    exports.reset = reset;

    if (_document.readyState == 'complete') {
        reset();
    } else {
        _window.addEventListener(
            'load',
            function () {
                reset();
            },
            0
        );
    }
});

/*jslint browser*/
(function () {
    'use strict';

    var replacement = function (matchedText, blockType, opacity, language) {
        opacity = opacity || 1;
        language = language === undefined ? 'plain' : language;
        var block = document.createElement(blockType);
        var textNode = document.createTextNode(String(matchedText));

        if (language !== 'plain' && language !== 'p') {
            block.classList.add('microlight');
        }
        block.appendChild(textNode);
        block.style.opacity = opacity;

        return block;
    };

    var backtickOpacity = 0.3;

    var codeFormatter = function (selector) {
        var contentArray = Array.prototype.slice.call(
            document.querySelectorAll(selector)
        );

        // Multi-line code:
        var tripleTickRegex = /```[\w\W]+```/gim;
        // Inline code:
        var singleTickRegex = /`[^`]+`/g;

        var codeLanguageRegex = /```{1}.*/;
        var tripleTickCapture = /(```)/;
        var singleTickCapture = /(`[^`]+`)/;

        contentArray.forEach(function (content) {
            if (
                !/<pre/gi.test(content.innerHTML) &&
                !/<code/gi.test(content.innerHTML) &&
                content.textContent
            ) {
                var theNewKidsOnTheBlock = [];
                if (tripleTickRegex.test(content.textContent)) {
                    // This needs a little extra filtering,
                    // but cascading is cool.
                    var codeLanguage = String(
                        content.textContent.match(codeLanguageRegex)
                    )
                        .slice(3)
                        .split(/(\s+)/)[0]
                        .trim();

                    var pairs = 0;
                    content.textContent
                        .split(tripleTickCapture)
                        .forEach(function (textBlock, index, self) {
                            if (tripleTickCapture.test(textBlock)) {
                                pairs++;

                                theNewKidsOnTheBlock.push(
                                    replacement(
                                        textBlock,
                                        'pre',
                                        backtickOpacity
                                    )
                                );
                            } else if (
                                self[index - 1] === '```' &&
                                self[index + 1] === '```' &&
                                pairs % 2 === 1
                            ) {
                                theNewKidsOnTheBlock.push(
                                    replacement(
                                        textBlock,
                                        'pre',
                                        1,
                                        textBlock.slice(0, 1) === 'p'
                                            ? 'plain'
                                            : ''
                                    )
                                );
                            } else {
                                theNewKidsOnTheBlock.push(
                                    replacement(textBlock, 'span')
                                );
                            }
                        });
                }

                if (singleTickRegex.test(content.textContent)) {
                    var singlePairs = 0;
                    var singlePlayerGame = function (splitBlock, index, self) {
                        if (singleTickCapture.test(splitBlock)) {
                            singlePairs++;
                            return replacement(
                                splitBlock,
                                'code',
                                backtickOpacity
                            );
                        } else if (
                            self[index - 1] === '`' &&
                            self[index + 1] === '`' &&
                            singlePairs % 2 === 1
                        ) {
                            return replacement(splitBlock, 'code');
                        } else {
                            return replacement(splitBlock, 'span');
                        }
                    };

                    if (theNewKidsOnTheBlock.length > 0) {
                        theNewKidsOnTheBlock.forEach(function (
                            block,
                            index,
                            self
                        ) {
                            if (singleTickCapture.test(block.textContent)) {
                                var newSet = [];
                                block.textContent
                                    .split(singleTickCapture)
                                    .forEach(function (child) {
                                        if (singleTickRegex.test(child)) {
                                            newSet.push(
                                                replacement(child, 'code')
                                            );
                                        } else {
                                            newSet.push(
                                                replacement(child, 'span')
                                            );
                                        }
                                    });
                                self[index] = newSet;
                            }
                        });

                        theNewKidsOnTheBlock = theNewKidsOnTheBlock.flat();
                    } else {
                        var theSingleKids = [];
                        theSingleKids = content.textContent
                            .split(singleTickCapture)
                            .map(singlePlayerGame);
                        var inlineSpan = document.createElement('span');
                        theSingleKids.forEach(function (singleKid) {
                            inlineSpan.appendChild(singleKid);
                        });
                        theNewKidsOnTheBlock.push(inlineSpan);
                    }
                }
                if (theNewKidsOnTheBlock.length > 0) {
                    while (content.firstChild) {
                        content.removeChild(content.firstChild);
                    }
                    theNewKidsOnTheBlock.forEach(function (newKid) {
                        content.appendChild(newKid);
                    });
                }
            }
        });

        // Invoke microlight:
        microlight.reset();
    };

    // Attach globally:
    window.codeFormatter = codeFormatter;
})();

codeFormatter('.content[contenteditable]');
ryanpcmcquen commented 4 years ago

Note that you will want the extension to still be disabled.

curiousepic commented 4 years ago

@ryanpcmcquen Appears to work properly with a similar test

ryanpcmcquen commented 4 years ago

I'm updating the Firefox and Chrome stores for Workflowy and The Google Keep code formatters now. Relevant release: https://github.com/ryanpcmcquen/codeFormatter/releases/tag/6.1.1

ryanpcmcquen commented 4 years ago

I've submitted updated versions for Chrome and Firefox of this and the Google Keep version with the following note for the reviewers:

This version fixes a CRITICAL BUG that caused data loss. Please publish it as soon as possible.

ryanpcmcquen commented 4 years ago

Firefox updated the same day but I am still waiting on Google to approve the new version.

ryanpcmcquen commented 4 years ago

The Chrome store has been updated as well.