変奏現実

パソコンやMMORPGのことなどを思いつくまま・・・記載されている会社名・製品名・システム名などは、各社の商標、または登録商標です。

この画面は、簡易表示です

[javascript]BNF

とっても古い2016年の記事に構文解析と云えば、BNF か ABNF か EBNFかな?作れたのは字句解析まで・・・orzが放置してたので、やってみた。

wikipediaのBNF表記ではうまく動作しないので、

<expression> ::= <list> | <list> "|" <expression>
<list> ::= <term> | <term> <opt-whitespace> <list>

空白の判定を必須とオプションに分け、listの式も面倒な方を優先させ、

<opt-whitespace> ::= /\x20*/
<whitespace>     ::= /\x20+/
<expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
<list>           ::= <term> <whitespace> <list> | <term>

と変更した結果、ソースはこんな感じになった。rule-nameの表現がクドいので変更した。

なお、BNFに空文や改行の無い行が含まれる場合はエラる。

    /**
     *  BNFルールと文法を作成
     * @param {string} bnf      BNFテキスト
     * @param {function} proc   mapのproc
     * @returns mapまたはbnfパターンの戻り値
     */
    parseBNF(bnf, proc) {
        const opt_whitespace = this.regexp(/\x20*/);
        const whitespace = this.regexp(/\x20+/);
        const regexp = this.map(this.regexp(/(\/(?!\\)[^/]*\/)/), (parsed) => { /* regexp */
            return parsed;
        });
        const dqstring = this.map(this.seq(this.token('"'), this.regexp(/(?!\\)[^"]*/), this.token('"')), (parsed) => { /* dqstring */
            return parsed.flat().join('');
        });
        const sqstring = this.map(this.seq(this.token(`'`), this.regexp(/(?!\\)[^']*/), this.token(`'`)), (parsed) => { /* sqstring */
            return parsed.flat().join('');
        });
        const literal = this.choice(dqstring, sqstring, regexp);
        const rule_name = this.map(this.seq(this.token('<'), this.regexp(/[A-Za-z0-9-_]+/), this.token('>')), (parsed) => {
            //  [0]:    <
            //  [1]:    rule - name
            //  [2]:    >
            return parsed[1];
        });
        const term = this.map(this.choice(rule_name, literal), (parsed) => { /* term */
            return parsed;                      // literal
        });
        const list = this.lazy(() => {
            return this.map(this.choice(this.seq(term, whitespace, list), term), (parsed) => { /* list */
                if (Array.isArray(parsed)) {        // term, whitespace, list
                    return parsed.flat();
                }
                return parsed;                      //  term
            })
        });
        const expression = this.lazy(() => {
            return this.map(this.choice(this.seq(list, whitespace, this.token('|'), whitespace, expression), list), (parsed) => { /* expression */
                if (Array.isArray(parsed)) {        //  list, whitespace, this.token('|'), whitespace, expression
                    return parsed.flat();
                }
                return [parsed];                      //  list  ruleの処理を簡素にするため配列で渡す
            })
        });
        const line_end = this.regexp(/[\r\n]+/);
        const rule = this.map(this.seq(opt_whitespace, rule_name, opt_whitespace, this.token('::='), opt_whitespace, expression, line_end), (parsed) => { /* rule */
            //  [0]:    opt_whitespace
            //  [1]:    rule_name
            //  [2]:    opt_whitespace
            //  [3]:    this.token('::=')
            //  [4]:    opt_whitespace
            //  [5]:    expression
            //  [6]:    line_end
            const rule_name = parsed[1];
            const expr = parsed[5];
            const expression = expr.filter((token) => token.replace(/\x20+/, '').length > 0);
            return { name: rule_name, expression: expression };
        });
        const syntax = this.lazy(() => {
            return this.map(this.many(this.choice(rule, this.seq(rule, syntax))), (parsed) => { /* syntax */
                const rules = {};
                if (parsed !== undefined && parsed !== null) {
                    while (0 < parsed.length) {
                        const p1 = parsed.shift();
                        rules[p1.name] = p1.expression;
                    }
                }
                return rules;
            })
        });
        this.fSpaceSkip = false;
        const result = syntax(bnf, 0);
        if (!result.success) {
            console.log('パース失敗');
        } else if (bnf.length !== result.position) {
            console.log(`${bnf.length}文字中の${1 + result.position}文字目でパース失敗`);
        }
        if (proc) {
            return proc(result);
        }
        return result.result;
    }
    testParseBNF() {
        const bnf = `<syntax>         ::= <rule> | <rule> <syntax>
                     <rule>           ::= <opt-whitespace> <rule-name> <opt-whitespace> "::="  <whitespace> <expression>  <line-end>
                     <rule-name>      ::= "<" /[A-Za-z0-9-_]+/ ">"
                     <line-end>       ::= /[\r\n]+/
                     <opt-whitespace> ::= /\x20*/
                     <whitespace>     ::= /\x20+/
                     <expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
                     <list>           ::= <term> <whitespace> <list> | <term>
                     <term>           ::= <rule-name> | <literal>
                     <literal>        ::= '"' <text> '"' | "'" <text> "'"  | "\`" <text> "\`" | <regexp>
                     <regexp>         ::= /.+/\n`;
        this.parseBNF(bnf, (parsed) => {
            const json = JSON.stringify(parsed.result, null, 2);
            const text = json
                .replace(/\[[\r\n]+/sg, "[")
                .replace(/(?!]),[\r\n]+/sg, ",")
                .replace(/[\r\n]+\x20+\][\r\n]+/sg, "]\n")
                .replace(/[\r\n]+(\s+\],)/sg, "$1\n")
                .replace(/\x20+/g, " ")
                ;
            console.log('result: ' + text);
        });

ソースの色付けが怪しいけど字句解析や文法系は一般的なソースコードの書き方ではないから仕方が無いか?(笑

ともあれ、これを動かして

result: {
 "syntax": [ "rule", "|", "rule", "syntax" ],
 "rule": [ "opt-whitespace", "rule-name", "opt-whitespace", "\"::=\"", "whitespace", "expression", "line-end" ],
 "rule-name": [ "\"<\"", "/[A-Za-z0-9-_]+/", "\">\"" ],
 "line-end": [ "/[\r\n]+/" ],
 "opt-whitespace": [ "/ */" ],
 "whitespace": [ "/ +/" ],
 "expression": [ "list", "whitespace", "\"|\"", "whitespace", "expression", "|", "list" ],
 "list": [ "term", "whitespace", "list", "|", "term" ],
 "term": [ "rule-name", "|", "literal" ],
 "literal": [ "'\"'", "text", "'\"'", "|", "\"'\"", "text", "\"'\"", "|", "\"`\"", "text", "\"`\"", "|", "regexp" ],
 "regexp": [ "/.+/"]
}

と出たので一段落。※RegExpなreplaceで短くなるように編集済み

とにかく、デバッグが大変。

一通り作ってから組み合わせてみて、動かしてもドコまで進行しているのかスグに判らなり迷子になってしまうので、tokenやregexpにブレークポイントを置いて、どの辺まで進行したのか?あたりを付けてから、通る様に文法を組み直した。

正規表現部分は、自身の /(\/(?!\)[^/]*\/)/ が通らないが、ほぼ同義の/.+/で通ったので良しとした。

mapやseqなどはspread-sheetカスタムエレメント3 のソースのまま。その元ネタは「Java パーサコンビネータ 超入門」で公開されたソースです。ソースを読んでもイミフなら元ネタを見た方がいいです。

あとは、各ルールで結果をちゃんとしたトークンに整えれば完成しそう。

    /**
     *  BNFルールと文法を作成
     * @param {string} bnf      BNFテキスト
     * @param {function} proc   mapのproc
     * @returns mapまたはbnfパターンの戻り値
     */
    parseBNF(bnf, proc) {
        const opt_whitespace = this.regexp(/\x20*/);
        const whitespace = this.regexp(/\x20+/);
        const regexp = this.map(this.regexp(/(\/(?!\\)[^/]*\/)/), (parsed) => { /* regexp */
            return { regexp: parsed.replaceAll(/\x20/g, "\\x20") };
        });
        const dqstring = this.map(this.seq(this.token('"'), this.regexp(/(?!\\)[^"]*/), this.token('"')), (parsed) => { /* dqstring */
            return { dqstring: parsed[1] };
        });
        const sqstring = this.map(this.seq(this.token(`'`), this.regexp(/(?!\\)[^']*/), this.token(`'`)), (parsed) => { /* sqstring */
            return { sqstring: parsed[1] };
        });
        const bqstring = this.map(this.seq(this.token('`'), this.regexp(/(?!\\)[^`]*/), this.token('`')), (parsed) => { /* bqstring */
            return { bqstring: parsed[1] };
        });
        const literal = this.choice(dqstring, sqstring, bqstring, regexp);
        const rule_name = this.map(this.seq(this.token('<'), this.regexp(/[A-Za-z0-9-_]+/), this.token('>')), (parsed) => {
            //  [0]:    <
            //  [1]:    rule - name
            //  [2]:    >
            return { rule_name: parsed[1] };
        });
        const term = this.map(this.choice(rule_name, literal), (parsed) => { /* term */
            return parsed;
        });
        //
        const flatArray = (parsed, typeName) => {
            if (Array.isArray(parsed)) {
                const stack = parsed.map((p) => {
                    p = flatArray(p, typeName);
                    if (p.type === typeName) {
                        return p.value;
                    }
                    if (p[typeName] !== undefined) {
                        return p[typeName];
                    }
                    return p;
                });
                return stack.flat();
            }
            return parsed;
        }
        const list = this.lazy(() => {
            return this.map(this.choice(this.seq(term, whitespace, list), term), (parsed) => { /* list */
                if (Array.isArray(parsed)) {
                    parsed = this.removeWhiteSpace(parsed);
                    const parsed0 = flatArray(parsed, "seq");
                    return { seq: parsed0 }; // term, whitespace, list
                }
                return parsed; //  term
            })
        });
        const expression = this.lazy(() => {
            return this.map(this.choice(this.seq(list, whitespace, this.token('|'), whitespace, expression), list), (parsed) => { /* expression */
                if (Array.isArray(parsed)) {
                    parsed = this.removeWhiteSpace(parsed);
                    if (parsed[1] === '|') {
                        const parsed0 = flatArray(parsed[0], "choice");
                        const parsed2 = flatArray(parsed[2], "choice");
                        return { choice: [parsed0, parsed2] };
                    }
                    return parsed; //  list, whitespace, this.token('|'), whitespace, expression
                }
                return parsed; //  list
            })
        });
        const line_end = this.regexp(/[\r\n]+/);
        const rule = this.map(this.seq(opt_whitespace, rule_name, opt_whitespace, this.token('::='), opt_whitespace, expression, line_end), (parsed) => { /* rule */
            //  [0]:    opt_whitespace
            //  [1]:    rule_name
            //  [2]:    opt_whitespace
            //  [3]:    this.token('::=')
            //  [4]:    opt_whitespace
            //  [5]:    expression
            //  [6]:    line_end
            const rule_name = parsed[1];
            const expr = this.removeWhiteSpace(parsed[5]);
            return { rule: { name: rule_name, expression: expr } };
        });
        const syntax = this.lazy(() => {
            return this.map(this.many(this.choice(rule, this.seq(rule, syntax))), (parsed) => { /* syntax */
                const rules = {};
                if (parsed !== undefined && parsed !== null) {
                    while (0 < parsed.length) {
                        const p1 = parsed.shift();
                        rules[p1.rule.name.rule_name] = p1.rule.expression;
                    }
                }
                return rules;
            })
        });
        this.fSpaceSkip = false;
        const result = syntax(bnf, 0);
        if (!result.success) {
            console.log('パース失敗');
        } else if (bnf.length !== result.position) {
            console.log(`${bnf.length}文字中の${1 + result.position}文字目でパース失敗`);
        }
        if (proc) {
            return proc(result);
        }
        return result.result;
    }
    /**
     * テスト
     */
    testParseBNF() {
        const bnf = `   <syntax>         ::= <rule> | <rule> <syntax>
                        <rule>           ::= <opt-whitespace> <rule-name> <opt-whitespace> "::="  <whitespace> <expression>  <line-end>
                        <rule-name>      ::= "<" /[A-Za-z0-9-_]+/ ">"
                        <line-end>       ::= /[\r\n]+/
                        <opt-whitespace> ::= /\x20*/
                        <whitespace>     ::= /\x20+/
                        <expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
                        <list>           ::= <term> <whitespace> <list> | <term>
                        <term>           ::= <rule-name> | <literal>
                        <literal>        ::= '"' <text> '"' | "'" <text> "'"  | "\`" <text> "\`" | <regexp>
                        <regexp>         ::= /.+/\n`;
        this.parseBNF(bnf, (parsed) => {
            const json = JSON.stringify(parsed.result, null, 2);
            const text = json
                ;
            console.log('result: ' + text);
        });

の結果がちょっと長いので最深部の{}は手作業で短縮形に編集したものがコレ。

result: {
    "syntax": {
        "choice": [
            { "rule_name": "rule" },
            {
                "seq": [
                    { "rule_name": "rule" },
                    { "rule_name": "syntax" }
                ]
            }
        ]
    },
    "rule": {
        "seq": [
            { "rule_name": "opt-whitespace" },
            { "rule_name": "rule-name" },
            { "rule_name": "opt-whitespace" },
            { "dqstring": "::=" },
            { "rule_name": "whitespace" },
            { "rule_name": "expression" },
            { "rule_name": "line-end" }
        ]
    },
    "rule-name": {
        "seq": [
            { "dqstring": "<" },
            { "regexp": "/[A-Za-z0-9-_]+/" },
            { "dqstring": ">" }
        ]
    },
    "line-end": { "regexp": "/[\r\n]+/" },
    "opt-whitespace": { "regexp": "/\\x20*/" },
    "whitespace": { "regexp": "/\\x20+/" },
    "expression": {
        "choice": [
            {
                "seq": [
                    { "rule_name": "list" },
                    { "rule_name": "whitespace" },
                    { "dqstring": "|" },
                    { "rule_name": "whitespace" },
                    { "rule_name": "expression" }
                ]
            },
            { "rule_name": "list" }
        ]
    },
    "list": {
        "choice": [
            {
                "seq": [
                    { "rule_name": "term" },
                    { "rule_name": "whitespace" },
                    { "rule_name": "list" }
                ]
            },
            { "rule_name": "term" }
        ]
    },
    "term": {
        "choice": [
            { "rule_name": "rule-name" },
            { "rule_name": "literal" }
        ]
    },
    "literal": {
        "choice": [
            {
                "seq": [
                    { "sqstring": "\"" },
                    { "rule_name": "text" },
                    { "sqstring": "\"" }
                ]
            },
            {
                "choice": [
                    {
                        "seq": [
                            { "dqstring": "'" },
                            { "rule_name": "text" },
                            { "dqstring": "'" }
                        ]
                    },
                    {
                        "choice": [
                            {
                                "seq": [
                                    { "dqstring": "\`" },
                                    { "rule_name": "text" },
                                    { "dqstring": "\`" }
                                ]
                            },
                            { "rule_name": "regexp" }
                        ]
                    }
                ]
            }
        ]
    },
    "regexp": { "regexp": "/.+/" }
}

大体予想通りの結果が得られたのでBNFについてはコレで終了。

※空白を”\x20″と表現させようとしたが、文字列中にバックスラッシュ(\)があると、テキスト化した時に(\\)に変換してるっぽい。



[三国英雄たちの夜明け]太守

もうちょっとで戦力5千万。

それにいつのまにか太守になってた。

そのせいかどうか判らないが不足していた食料何となく溜まり始めたのでとても助かります。

今、気になってるのが「一定確率で獲得」できるハズの特産を貰ったコトが一度も無い事。

近くの港を占領してなかったせいかな?

と思いつき今、占領してみた。

確かにこの港で貿易すると↑のアイテムが出やすいかも?。(太守特典?



[javascript]async await 連鎖

  • ドコかで同期を取るために awaitする
    • awaitを使用するためにその関数にasyncを付ける。
      • その関数を呼び出す関数も概ね後処理があるのでawaitする
        • awaitを使用するためにその関数にasyncを付ける。
          • …延々と連鎖

セルに表示するデータをindexedDBで取得するように修正したら・・・↑の様なことになった。

サクラエディタに関数とソレを参照する関数を階層リストを書いて漏れチェック。

幸いにもコノ程度で済んだ。

  • spreadSheet:getCellData
    • layout:createThTdElement
      • layout:createTrElement
        • layout:createTableElement
      • layout:createColThTdElement
        • layout:insertOverCol
          • command:moveCell
            • event:clickMouseEvent
            • layout:makePageContents
              • spreadSheet:makePageContents
    • layout:getCellData
      • command:openEntry
      • layout:isCellData
        • command:chkDirectionData
  • spreadSheet:setCellData
    • layout:setCellData
      • command.closeEntry
        • event:dblclickMouseEvent

そこまでは良かったが動作が変。

地道にチェックしていくと、Arrayのsomeのコールバックがasyncな場合でも同期を取らず、どんどん処理を進めていくコトが判明。

対処方法としては new PromiseでPromiseの配列を作ってPromise.allで完了待ちが筋だが、

const objRow = createHtmlElement('tr', attrs);
let w = 0;
if (nRow === 0) {//初期表示時   ※TODO 初期表示時は左上セルがA1と仮定
    const cols = this.spreadSheet.cols;
    for (let nCol = 0; nCol <= cols; nCol++) {
        const cell = await this.createThTdElement(objRow, nRow, nCol);
        w += cell.clientWidth;
        if (rectParent.width < w) {
            break;
        }
    }
}

外の変数を使っている場合が多々あったのでsome化したものをfor文に戻した。

rangeをPromise対応してみようか・・・

/**
 *  連番配列っぽいイテレータっぽいものを作成する
 * @param {integer} start           開始番号
 * @param {integer} end             終了番号
 * @returns {iterator}              開始番号から終了番号を格納した配列っぽいイテレータ
 */
const range = (start, end) => {
    let step = 1

    const rangeIterator = {
        some: function (func) {//someっぽく動作させる
            for (let cnt = start; cnt <= end; cnt += step) {
                let rc = func(cnt);
                if (rc) {
                    return true;
                }
            }
            return false;
        },
        map: function (func) {//mapっぽく動作させる
            const f = false;
            let aRc;
            if (f) {
                aRc = new Array(end - start + 1);
            }
            let index = 0;
            for (let cnt = start; cnt <= end; cnt += step) {
                const rc = func(cnt);
                if (aRc) aRc[index++] = rc;
            }
            return aRc;
        },
    };
    return rangeIterator;
}

無理っぽいなぁ~

/**
 *  連番配列っぽいイテレータっぽいものを作成する
 * @param {integer} start           開始番号
 * @param {integer} end             終了番号
 * @param {integer} step            ステップ
 * @returns {iterator}              開始番号から終了番号を格納した配列っぽいイテレータ
 */
const promiseRange = (start, end, step = 1) => {
    return rangeIterator = {
        some: function (func) {//someっぽく動作させる
            return new Promise(async (resolve, reject) => {
                for (let cnt = start; cnt <= end; cnt += step) {
                    let rc = await func(cnt);
                    if (rc) {
                        resolve(true);
                        return;
                    }
                }
                resolve(false);
            });
        },
        map: function (func) {//mapっぽく動作させる
            return new Promise(async (resolve, reject) => {
                let aRc = [];
                for (let cnt = start; cnt <= end; cnt += step) {
                    const rc = await func(cnt);
                    aRc.push(rc);
                }
                resolve(aRc);
            });
        },
    };
}

こんな風に使うんだけど

const cols = this.spreadSheet.cols;
await promiseRange(0, cols).some(async (nCol) => {
    const cell = await this.createThTdElement(objRow, nRow, nCol);
    w += cell.clientWidth;
    if (rectParent.width < w) {
        return true;
    }
    return false;
});

動くから良し。

ps.

だがしかし、後でasync付きのメソッドを呼び出す時にawaitし忘れガチ。なので、メソッド名のツールチップを見て戻り値がPromiseになってないか確認しないと同期ズレで酷い目に遭う。asyncは明示しないとスクリプト側が大変だと思うけど、awaitは暗黙の了解ぽく自動的に処理して欲しいなぁ。代わりに noWait実装でヨロ。

ps.2024/3/18

計算式にセル参照を追加したら、またasync/awaitの連鎖。しかし全般的にパーサやコールバックをasyncし、呼び出し側でawait するダケで済む。かと思ったが、Array.reduceでPromiseを回すと、パーサーコンピネータなので、

Array.reduce
  return new Promise((resolve,reject)=>{
 promise,then(()=>{
   ・・・
  Array.reduce
    return new Promise((resolve,reject)=>{
     promise,then(()=>{
     ・・・
    Array.reduce
       return new Promise((resolve,reject)=>{
        promise,then(()=>{
       ・・・
         resolve(xxx);

と各所(chiceやseq等で)階層ループになってしまう。突入するのはいいが、離脱する際に、どのArray.reduceに戻るか不安定。function () {…}.bind(this)のbindのthisとPromise.resolve(…)は仕様的にも相性が最悪だけど、reduceか階層ループのいづれかが無ければ、どうと云うことは無いので、Array.reduce(…)をfor(){…}に戻した。

その後、セル参照のある数式を連鎖して再計算させてみたら、indexedDBのトランザクションの階層的な利用は不可だったので、階層化しそうな処理をsetTimeoutでばらまいたが、トランザクションが終わらないうちに動き出すので、一旦配列に入れトランザクション完了後に、配列をpopで読出しつつsetTimeoutでばらまいた。そうするしかない。順序を守りつつ非同期で処理しないとトランザクションが階層化してしまうからね。



「javascript]謎な不安定さ

contenteditableのカスタムエレメント版 の修正も大方終わった。

気がするけど、見る度に手直ししている。(大笑

‘”.*?”‘では、リテラル中のバッククォートを処理できない。なので、JDOCやコメント部はリテラルとと一緒に処理し、その他は字句解析し直すことにした。※convertJavascriptToHTML1~2

javascriptでは、'”(\\\\.|.)*??”‘がエラるが、'”(\\\\.|.)*?”‘なら通るので、matchの戻り値[0]のテキストを使い括弧で括ってないパターンの部分も取れる。※convertJavascriptToHTML3

多分これで大丈夫。(でも心配だから、1~2も残しておく。

JDOCも変になっていた。最後の字句解析で行ごとにspanで括る様にしたので再帰したりすると変更漏れ

ちょっと修正すると動作がガタガタになりやっと安定すると行番号の幅が変に狭いので見てみるとVisualStudioCode??が謎に}を追加しててcssが効かなくなってたりするのは比較的解りやすいけど…

`.code>.row2 {
    margin-left: 1.5rem;
} ☚❓
}\n`,
`.code>.row3 {
    margin-left: 2rem;
} ☚❓
}\n`,

そのガタガタ動作を追っていった最中に気が付いたコトをメモっておく。

staticな初期設定あるいはthisの話

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
}

は上手く動作するけど、staticにすると

class Sample {
  static _ = Sample.setup();
  static styles={};
  Sample.setup(){
    Sample.styles['one'] = 1;
    Sample.styles['two'] = 2;
    Sample.styles['three'] = 3;
  }
}

と初期設定をした後に

static styles={};

が動き出す場合があるので、

class Sample {
  static _ = Sample.setup();
  static styles;
  Sample.setup(){
    if(!Sample.styles) { Sample.styles={}; }
    Sample.styles['one'] = 1;
    Sample.styles['two'] = 2;
    Sample.styles['three'] = 3;
  }
}

の様に各static要素の初期設定順に依存しないようにした方がいい。

また、

class Sample {
  static _ = Sample.setup();
  static setup() {
       Sample.field1 = 10;
  }
  static field1;

とクラス名のトコにthisに変えてもstatic同士なら支障は無い。

class Sample {
  static _ = this.setup();
  static setup() {
       this.field1 = 10;
  }
  static field1;

がインスタンスを得た時点でthis経由でstaticなものは見えないので動きが変になりやすい。

更にサブクラスからSample.setup()の様に呼び出されると

class Sample {
  static _ = this.setup();
  static setup() {
       this.field1 = 10;
  }
  static field1;

class SubSample extends Sample{
  static _ = Sample.setup();

サブクラス側にstaticなfield1が出来てしまう様だ。

コレクション型フィールドのgetterの戻り値を使いまわす場合

一番悩ましいのはコレ。

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  getStyles() { return this.styles; }
}

・・・

class SubSample extends Sample {
    ・・・
    const styles = this.getStyles();
    {
    styles['four'] = 4;
      {
      styles['five'] = 5;
        {
          styles['six'] = 6;
             ・・・
               {
               styles['ten'] = 10; 
  ※もうこのあたりまでくるとthis.getStyles();との距離がかなり離れてしまい
  this.stylesに更新した内容(=4のあたりから=10まで)が反映されない時がある。

単にソースが長くなってくると起きることがあるので、

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  getStyles() { return this.styles; }
}
・・・
class SubSample extends Sample {
・・・
    const list = {
    'four': 4,
    'five': 5,
    'six': 6,
・・・
    'hundred': 100,
    };

な感じで

    const styles = this.getStyles();
    Object.keys(list).forEach((key) => styles[key] = list[key]);

とできるダケ「this.getStyles()」を近づけて逃げるよりも

    Object.keys(list).forEach((key) => this.styles[key] = list[key]);

this.stylesに直接入れるか、処理の戻り値でthis.stylesを上書きしてしまった方が良い。

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  setStyles(v) { this.styles = v; }
  getStyles() { return this.styles; }
  ・・・・
  method() {
    ・・・・
    const styles = this.getStyles();
    ・・・・
  this.setStyles(styles);
  }
}


[javascript]クラスの名前を調べる

ログウインドウの右端にファイル名が付くけど、クラス名も書き出したい。

class xxxx{
  static className = this.name;
  ...
}

は安定してクラス名が取得できるが、これは、staticなメソッドのみ参照可能で、インスタンス依存のメソッド用に

class xxxx{
  className;
  constructor() {
    this.className = this.name;
    ...
  }
  ...
}

class xxxx{
  className;
  constructor() {
    this.className = this.constructor.name;
    ...
  }
  ...
}

のいづれかが必要だった。どっちが常に正しいという訳では無いので、

static className = this.name;
className;
constructor() {
  this.className = this.name ?? this.constructor.name;
  ...
}

~なぁあたりが良さそうだ。

??はあまり使わないけど、Null 合体演算子で、

「とりあえず【まともな値】を返す」場合に便利な演算子。

新しいパターンが増えたら

static className = this.name;
className;
constructor() {
  this.className = this.name ?? this.constructor.name ?? {新しいパターン};
  ...
}

にすればいいだろう。

これで各メソッドに

    メソッド名 (...) {
        const methodName = '{メソッド名}';
        console.debug(`${this.className}.${methodName}(...)`);

と書ける。

例外処理を発生してスタックを調べる事もできたけど、イマイチ感がある。

const getCurrentLineInfo = () => {
    try {
        throw new Error('getCurrentLineInfo Error');
    } catch (ex) {
        let stack = ex.stack.split('\n');
        let target = stack[2];
        // パターン1:at xxClass.yyMethod (https://{domain}/{path}/{filename}:{line}:{column})
        // パターン2:at xxClass.yyMethod (file:///{drive}/{path}/{filename}:{line}:{column})
        let match = target.match(/\s*at\s*([_a-zA-Z]+)[.]([_a-zA-Z]+)\s*([(])(.+?)([)])/);
        // /\s*at\s*([_a-zA-Z]+)[.]([<>_a-zA-Z]+)\s*(?<=[(])(.+?)(?=[)])/ にすると失敗?
        let className = match[1];
        let methodName = match[2];
        let url = match[4];
        url = url.split(':');
        let type = url.shift();
        switch (type) {
            case 'http':
            case 'https':
                var domain_fullpath = url.shift();
                var [_, _, domain, ...fullPathName] = domain_fullpath.split('/');
                fullPathName = '/' + fullPathName.join('/');
                var row = url.shift();
                var column = url.shift();
                break;
            case 'file':
                var drive = url.shift();
                var fullPathName = url.shift();
                var row = url.shift();
                var column = url.shift();
                break;
        }
        let fileName = fullPathName.split('/').slice(-1)[0];
        let fullPath = fullPathName.substring(0, fullPathName.length - fileName.length - 1);
        var rc = {
            type: type,
        }
        if (type == 'file') {
            Object.assign(rc, {
                drive: drive,
            });
        } else {
            Object.assign(rc, {
                domain: domain,
            });
        }
        Object.assign(rc, {
            fullPath: fullPath,
            fileName: fileName,
            className: className,
            methodName: methodName,
            row: row,
            column: column,
        });
    }
    return rc;
}
  • 名前を調べる目的の割に長すぎる
  • 関数を呼ぶ度に例外処理を起こすのは実行時間に響きそう
  • chromeのスタック情報に依存したコードなので他ではエラりそう

サンプルでボタンを押すと表示する。

getCurrentLineInfo.jsのSampleClassクラスのexec()を呼び出した場合、
{
 "type": "https",
 "domain": "ssiscirine.iobb.net",
 "fullPath": "/sample/stack",
 "fileName": "getCurrentLineInfo.js",
 "className": "SampleClass",
 "methodName": "exec",
 "row": "5",
 "column": "12"
}
getCurrentLineInfo.jsのgetCurrentLineInfo()を直接呼び出した場合、
{
 "type": "https",
 "domain": "ssiscirine.iobb.net",
 "fullPath": "/sample/stack",
 "fileName": "stack.html",
 "className": "HTMLButtonElement",
 "methodName": "<anonymous>",
 "row": "14",
 "column": "43"
}

FireFoxではクラス名は無いけどメソッド名(関数名?)まで取れるのでサンプルの方は対応している。

HTMLのscriptやonclickからgetCurrentLineInfo()を直接呼び出した場合は行列位置は正しいので使えるけど、他は当てできないね。

それにしても

out.innerText = JSON.stringify(getCurrentLineInfo(),null," ");  ※" "は全角1文字

と、インデントを指定すると、改行もしてくれるので読みやすい。※”\t”も良いらしい。



[javascript]spread-sheetカスタムエレメント

spread-sheetカスタムエレメントってありそうで無いっぽいので作ってみた。

セルに文字入力できるダケのしろもの。

  • IMEがONになると動きがおかしい
  • 範囲指定ができない
  • 計算できない
    • 簡単な計算は可、セルアドレスでセル値参照も可
    • 関数が何も無い
      • sum,count,average,min,max
    • 値の変動に計算式を連動
      • 範囲指定時の連動が未実装
  • 入力したデータの保存ができない
    • indexedDBにセル値を保持
      • indexedDBのスコープがドメイン単位なので別ページと被ってしまいがち
        • databasse属性にurlから置換する%domain%,%path%,%filename%を追加
          • 例:databasename=”%domain%%path%%filename%_スプレッドシート1”
            • 結果はDevToolsのアプリケーションのindexedDBを参照
  • セル幅や高さが変えられない
  • 文字のスタイルを変えられない
  • javascriptからシートのデータを参照できない
  • ステータスバーが無い
  • 数式入力バーが無い
  • セルを移動し画面外に出ても画面がスクロールしない
    • 行列タイトルがスクロールロックしてない
      • スクロールバーが無反応
  • マクロが無い
  • データのインポート、エクスポートが無い
  • グラフ、絵、矢印とか貼れない
  • セルに名前が付けられない
  • シートが1枚ダケ
  • 列・行の追加や削除ができない
  • カットアンドペーストができない

等々いっぱい未実装。

1つのクラスでは長すぎるので分割してみた。

  • SpreadSheetCustomElement   全体(this)
    • this.customElement     カスタムエレメント登録Callback等
    • this.event         イベント処理
    • this.command       コマンドっぽい処理
    • this.layout         レイアウト処理

各処理からはthis.spreadsheet経由で別処理を呼び出す必要がある。

function OpenEntry () {
  let rect = this.spreadsheet.layout.cellEditing.getBoundingClientRect(); //編集するセルの位置を求め
  this.spreadsheet.layout.divTextarea.top = rect.top; //テキストエリアを内包するdivをソコに移動させ
  this.spreadsheet.layout.divTextarea.left = rect.left;
  this.spreadsheet.layout.textarea.value = event.key; //テキストエリアに入力した文字を入れ
  this.spreadsheet.layout.divTextarea.style.display = 'block'; //テキストエリアを内包するdivを表示
}

this.xxxx.yyyy.zzzzと長々と詠唱するのは読みにくいので

function OpenEntry () {
  const spreadsheet = this.spreadsheet;
  const layout = spreadsheet.layout;
  let rect = layout.cellEditing.getBoundingClientRect(); //編集するセルの位置を求め
  layout.divTextarea.top = rect.top; //テキストエリアを内包するdivをソコに移動させ
  layout.divTextarea.left = rect.left;
  layout.textarea.value = event.key; //テキストエリアに入力した文字を入れ
  layout.divTextarea.style.display = 'block'; //テキストエリアを内包するdivを表示
}

constを使い短縮化した。

後で割り当てを変更する時には直しやすいかもしれない。(気がする。

this.spreadsheet.layout.divTextarea.top 
// divTextareaをlayoutからshadowへ移動
=>this.spreadsheet.layout.spreadsheet.shadow.divTextarea.top 
// divTextareaをshadowからlayoutへ戻す
=>this.spreadsheet.layout.spreadsheet.shadow.spreadsheet.layout.divTextarea.top 
// は?

となるよりは

layout.divTextarea.top 
// divTextareaをlayoutからshadowへ移動
=>shadow.divTextarea.top 
// divTextareaをshadowからlayoutへ戻す
=>layout.divTextarea.top 
// 良し!

の方がマシ。

さて!いざ、やってみるとVScodeのリファクタリングが不十分でサクラエディタで(ガシガシ

になるのはいつものこと。

カスタムエレメントの宣言をクラスの出す事は可能だけど、クラスのstatic getterがconstructorに作られるので差し替え不可、無名のクラス文でラップするとコードがとても長くなるので取止め。

ps.

行列タイトルロックできたけど、タイトルロックの状態の表と元のサイズの表の差をスクロール量としてスクロールバーのスライダー位置を調整しているので、まだスライダーを触ってもタイトルロックの状態は変動しない。

ps

spread-sheetカスタムエレメント3 セルの範囲をMS-Office365のExcelまで拡大してみたところ、Ctrl+↓↑で移動すると1,048,576行もあるせいで移動完了まで「しばらくお待ちください」状態。カーソルをあちこちと移動するとテーブルが右端から壊れていく。

ps.

多少良くなったので、spread-sheetカスタムエレメント3を更新、データもindexedDBに保持したので、ブラウザが重くなるかもしれない。また時々DB構成を変えるかもしれないので、devToolsのアプリケーションでストレージからIndexDBに▶が付いていたらDB削除してから見た方が良いかも。まだカーソルジャンプ中に帰ってこない時がある。

ps.

indexedDB周りをdatabase.jsに分離。データベースも’R0000001’とか’C00002’など固定文字列も止めてinteger化したが今までどおりVersionアップ処理はしていない。

ps.

A1セルから右端まで移動し一番下の行へジャンプするとセルが隠れた。同様に一番下の行のA列から右端の列までジャンプし一番上の行へジャンプすると右側の隙間がひろがっていた。この修正中に列を追加する処理を呼び出す側がawaitしていなかったせいで画面が壊れることがあった。

カスタムエレメントの属性にdatabasenameを追加し、スプレッドシート毎にindexedDBのデータベースを分離可能に。

ページ移動(PageUp/Down)で同期を取っていたら、キーリピートで並行に処理が走ってしまい画面構成がクズれるので、全般的にキー操作はsetTimeoutで後回しにすることで並行に処理が走らない様にした。

ps.2024/3/18

indexDBの構成を変更!パーサーコンビーネータを組み込んで数値やセルアドレスで四則演算程度の数式を計算できた。但し、indexedDBが非同期なので、パーサーコンビーネータでArray.reduceを使った箇所の同期ズレ(Promiseレーシングや無限ループ)を対処できずfor文に戻した。

セル値の変動に連動して再計算させてみたが、inndedDBのトランザクションは階層化できない様で、トランザクションを流しながらセルデータをinndedDBで読み書きした後に、カーソルを移動させると、トランザクションが終了な済みエラーになる。また、データベースに複数キーのインデックスがあると、単キーのインデックスでもカーソルの範囲(上下限)は配列で指定しなければいけない。bound([1],[1])でデータが取れるけど、bound(1,1)ではスカっと終了する。

そう云えば、数式とかデータに空白が混入するとパーサのパターンにマッチしない。パーサの処理の先頭に空白の読飛ばし処理を追加した。でも数式は先頭文字が=の場合だけに限定したいので、読飛ばしをON・OFFするようにした。seqとchiceにパラメータに混入させたのでソコはチョットごまかし。

#初期設定
this.fSpaceSkip = false;
・・・
#数式の構文のルート木
const exprCell = this.map(this.seq(this.token('='), () => { this.fSpaceSkip = true }, expr), async (parsed) => {
・・・
#値の構文のルート木
const cell = this.choice(exprCell, () => { this.fSpaceSkip = false }, valueCell);
・・・
#各パーサの処理
async (target, position) => {
  if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
・・・
}
#seqまたはchoiceのforループ
for (var i = 0; i < parsers.length; i++) {
  var parsed = await parsers[i](target, position);
  if (parsed === undefined) {
    continue;
  }
  ・・・
}

TextAreaを表示中に他のセルをクリックするとTextAreaを閉じそのセルをフォーカスっぽい表示にするけど、その際にクリックイベントで渡されたevent.targetの中身が書換わってスクリプトが暴走状態になってたので即保持しておく。※Promiseを使い出してからはよく起きる現象

ps.2024/3/19

sum関数がちょっと動き出した。なのでindexedDBの構成がまた変わった。min.max,averageもセルの範囲指定でも計算可。※結果が正しいかもしれない。

ps.2024/3/20

indexedDBのセルデータをJSON風に変更で文字列型が修正漏れ。sum関数等でほぼ同じコードが続くのでコールバック式に変更。Deleteキーでセルデータを空白に。=A1などアドレスや範囲が結果になる場合にそれっぽく表示。1+2は計算するけど1+2+3は面倒そうなので後回しにしたら、関数のパラメータも3個あるとダメだったし、アドレスに小文字を使うと変で、=A1:C7と範囲値の場合も中途半端で、セルを空欄にするとundefined(ある意味正しいけど)になってたので、見直し。確認のためにindexedDBを見ると、画面からデータを削除するとレコードの値が空になってたダケだったのでレコードごと消える様に修正。indexedDBのデータが同じドメインなら共有だがdatabasename属性に置換文字列を追加し名前で切り分け可能にした。

ps.2024/3/21

セルデータをJSON風化のmin.max関数の修正漏れ。(v)?v:”でnullやundefinedが表示されない様にしたら0も表示しなくなっていた。date型で日付と時刻の入力でDate型のformatを実装、boolean型の入力、now(), today()を実装。論理式を実装しないとif関数が作れない。round()とvlookup()はコードしてみたダケ。

ps.2024/3/24

a1形式のaddress1をaddressに。関数をグループ管理に変更中。後、細かいバグ。セル上にマウスを移動するとデバッグ用ツールチップを表示。

ps.2024/3/25

データ型をstringをtextに変更。F2で入力すると直前のセルで入力した内容がチラっと見えていた。

パーサーコンビネータに論理式等を追加したのでexpr(計算式の根っこ)の呼び出しが激増しasync、 awaitも多く連動計算の待ち時間が長くなってきてsetTimeoutで連動計算を呼び出していることもああり、僅かなミスでスタックオーバーフローやメモリ不足に陥りやすくなっている。

ps.2024/3/26

計算式のパース(parse)と計算(calc)を分離、indexedDBの式はパース結果(トークン風)。これでA1とR1C1の切り替えができそう。少し寄り道してmapメソッドをBNFテキストからパーサーコンビネータの文法部分を作るようにしてしまいたい。今は this.map(this.seq( のようにthis と括弧が多いから。

ps.2024/3/27

演算子*と/が+ーな計算をしていた。関数をexprメンバーに文字で格納していた名残で式のトークンを配列で格納していたがname,parametersに分けた。式を再入力するとexprがname,parametersに変わる様に暫くは式の文字列化と計算でexprも併用させる。アドレス型のaddressを配列に変更したのでそのうちアドレス範囲型を無くす予定。



[javascript]スクロールバーのサイズ

気になると夜しか眠れないスクロールバーのサイズ

Windowsでは画面の解像度などで異なるし、古いXP等では「画面のデザイン」でも変更可能だった。

なので状況に応じて調べるしかない。

HTMLElementにスクロールバーを付けて

div.sample {
    overflow-x: scroll;
    overflow-y: scroll;
}

スクロールバーのサイズ = スクロールバーを含む領域 ー スクロールバーを含まない領域

から求められる。

具体的には

let div = document.querySelector('div.sample');
let rectDiv = div.getBoundingClientRect();
let scrollbarSize = {
    width: rectDiv.width - div.clientWidth,
    height: rectDiv.height - div.clientHeight
};

注意点としては

いつでも正しい数値が得られる訳では無いので

scrollbarSizeのwidthかheightのいづれかが0の場合は再トライが必要。

                //  タイミングによってはwidthが0になるため再処理
                if (scrollbarSize.width === 0 || scrollbarSize.height == 0) {
                    return this.resizeDivTable(0);
                }

ブラウザのDevTools上で【F5】を押すと無限ループしやすいので再トライ数を制限した方がいい。



[javascript]childrenを全削除したい

古くからある問題に列挙されたリストを全削除したい場合・・・

for (let index = 0; index < shadow.children.length; index++) {
    shadow.children[index].remove(); あるいは shadow.removeChildren(shadow.children[index]);
}
console.log('remain object:' + shadow.children.length;
>> remain object:1.

と残ってしまいがち。

リストを先頭から順に削除しているのでイテレータの思いとのすれ違いが起きてしまう。

while (0 < shadow.children.length) {
    shadow.children[0].remove(); あるいは shadow.removeChildren(shadow.children[0]);
};
console.log('remain object:' + shadow.children.length + ".");
>> remain object:0.

この様にイテレータを使わないのが正解だけど、[0]のマジックナンバーは~とか云われそう。

while (0 < shadow.children.length) {
    shadow.firstChild.remove();
};
console.log('remain object:' + shadow.children.length + ".");
>> remain object:0.

の方がいいかな?

for (let ch in shadow.children) {
    ch.remove();
>> Uncaught TypeError TypeError: ch.remove is not a function
};
console.log('remain object:' + shadow.children.length + ".");

とすると、最後の方でchに’length’が割り当てられて、TypeErrorが起きてしまうのはお約束。

for (let ch of shadow.children) {
    ch.remove();
};
console.log('remain object:' + shadow.children.length + ".");
>> remain object:1.

で、エラーは起きないけど、やはりイテレータとのすれ違いで消し残りが出てしまう。

今なら

Array.from(shadow.children).forEach(ch => ch.remove());
console.log('remain object:' + shadow.children.length + ".");

が良さそうかな?fromの名前がちょっとアレだけど。(笑

shadow.innerHTML= '';

やっぱりコレか!

でもいっぱいエレメントが入っていると遅いらしい。

var clone = shadow.cloneNode( false );
>> Uncaught DOMException DOMException: Failed to execute 'cloneNode' on 'Node': ShadowRoot nodes are not clonable.
shadow.parentNode.replaceChild( clone , shadow );

親エレメントの方ですげ替えした方が速そうだけど・・・

shadowでは無理



[javascript]QuerySelectorAllの妙な動き

<html>
・・・
<body>
  <br>
  <br/>
  <br>
  <br/>
</body>

</html>

これを

document.querySelectorAll('br')

すると

Array.from(document.querySelectorAll('br')).map((br)=>br.outerHTML)
0:"<br>"
1:"<br>"
2:"<br>"
3:"<br>"
length: 4

となる。

しかし自作タグ(カスタムなタグ)の場合は、

<html>
・・・
<body>
    <sample-element></sample-element>
    <sample-element />
    <sample-element></sample-element>
    <sample-element />
</body>

</html>
Array.from(document.querySelectorAll('sample-element')).map((sample)=>sample.outerHTML)
0:"<sample-element></sample-element>"
1:"<sample-element>\n    <sample-element></sample-element>\n    <sample-element>\n\n\n</sample-element></sample-element>"
2:"<sample-element></sample-element>"
3:"<sample-element>\n\n\n</sample-element>"
length: 4

ブラウザのDevToolsも同様なので、HTMLファイルの読み込み時点で【そのように解釈】されているので仕方が無いんだけどね。(笑

このため、ちゃんと「Autonomous custom element(自律カスタム要素)」として書いても結果は変わらない。(ハズ



[javascript]returnした後に

return  (fValue)?[nCount[name]= 1:nCount[name];
nCount[name]++;

以前はreturn文は戻り値をスタックするだけで、後ろに処理があれば実行してくれた。(時期もあった

でも、今はそんなことは無いので・・・

return (fValue)?[nCount[name]= 1, nCount[name]++][0]: nCount[name]++;

と変な書き方をすることがマレにあるけど

return (fValue)?nCount[name]= 1: ++nCount[name];

の方がマシかな




top