画像フォルダのファイルを連番に書き換える

PHP

PRTFileName ('./data/*.jpg');
exit ();

//指定のディレクトリのファイル名を順序だてて再構成する(新しい順)
function PRTFileName ($path, $format = 'img_%04d.jpg', $limit = 10000) {

  $files = [];
  $cnt = 0;

  //$pathのチェック
  $info = pathinfo ($path);
  if (false === is_dir($info['dirname']))
    throw new Expression ('No directory!');

  //ディレクトリ内のファイル名の取得と日時を配列にする
  foreach (glob ($path) as $file)
    $files[$file] = filemtime ($file);  

  //日付順でソート
  arsort ($files);
  //ファイル名で再構成 & ここで最大数で切り取り
  $files = array_slice (array_keys ($files), 0, $limit -1);

  //拡張子がtmpでリネーム
  foreach ($files as &$fName) {
    $f = pathinfo ($fName);
    $nName = sprintf ('%s/%s.tmp', $f['dirname'], $f['filename']);
    rename ($fName, $nName);
    $fName = $nName;
  }

  //書式に従ってファイル名を書き直す
  foreach ($files as $fName) {
    $nName = sprintf ('%s/'.$format, $info['dirname'], $cnt++);
    rename ($fName, $nName);
  }

  return count ($files);
}

データベースに記録するテーブルの構造を可変にしたい

データ(レコード)サイズが大きいわけではない。
テーブル構造(フィールド)を頻繁に変更するかもしれない。

ということで、JSON形式にして文字列のまま保管できるようにしたい。
HTMLのFORMの中の要素も可変にしたい

JSONを利用して(テーブル式)フォームを形成する

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title></title>

  <meta http-equiv="content-language" content="ja">
  <meta http-equiv="Cache-Control" content="no-cache">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="format-detection" content="telephone=no">
  <link rel="stylesheet" href="form_table.css" type="text/css">
  <style>
  </style>
</head>

<body>
<form method="post" enctype="multipart/form-data" id="FF">
  <table id="T"></table>
  <button onclick="console.log($F.getValue(),FF); return false;">フォームの値を取得</button>
  <input type="submit" value="submit">
</form>

<script>
const JSON = [
  {title:['世帯名',{rowspan:1}], name:'SETAI'},
  {title:'住所', name:'POSTCODE', attr:{placeholder:'〒'}},
  {name:'ADD' },
  {name:'ADD' },

  {title:'電話', name:'TEL'},
  {name:'TEL' },
  {name:'TEL' },

  {title: '性別', name:'SEIBETU', type: 'radio', option: [
    ['男性','man', 0,1],
    ['女性','woman'],
    ['不詳','',1,0],
  ]},

  {title: '言語', name:'GENGO', type: 'checkbox', option: [
    ['PHP','PHP'],
    ['JavaScript','JS',1],
    ['BASIC','BASIC'],
  ]},

  {title: 'IQ', name:'IQ', type: 'select', option: [
    ['--選択して下さい--','',0],
    ['頭が良い','120'],
    ['普通','100'],
    ['無理','80'],
  ]},

  {title: '地区', name:'TIKU', type: 'datalist', option: [
    '種市','小子内','有家','中野','大野',
  ]},

  {title: 'Memo', name:'MEMO', type: 'textarea', attr:{rows:10}},



//  {title: '', name:'', value: '', type: null, option: [], attr: {}},

];



class $F {
  /*
    独自のJSON形式で模したフォーム要素を具現化してテーブル形式にフォームを構成する
  */
  static mkTable (aryArg = [], tbody = document.createElement ('tbody')) {
    const E = (n,a={},c=[],e)=> (e=document.createElement(n),Object.entries(a).forEach(([a,b])=>b !== false && e.setAttribute(a,b)),e.append(...c),e);

    let tr, th, attr, cnt = 0;
    for (let obj of aryArg) {
      let t = obj.title;
      tr = tbody.insertRow ();
      if (null != t) {
        if (1 < cnt)
          th.rowSpan = cnt;

        tr.append (th = document.createElement ('th'));
        if (Array.isArray (t)) {
          [t, attr] = t;
          Object.entries (attr).forEach (_=> th.setAttribute (..._));
        }
        th.textContent = t;

        cnt = 1;
      } else
        cnt++;
      
      this.mkInput (obj, tr.insertCell ());
    }
    return tbody;
  }

  /*
    フォーム要素を生成

  */
  static mkInput (mold, parent = new DocumentFragment ()) {
    // Object.assign では list属性を変更できないので setAttribute を利用
    const E = (n,a={},c=[],e)=> (e=document.createElement(n),Object.entries(a).forEach(([a,b])=>b !== false && e.setAttribute(a,b)),e.append(...c),e);

    let {name, value = '', attr = {}, option = [], type = 'text'} = mold;
    let e = [], id;
    type = mold.type = type.toLowerCase ();

    switch (type) {
    case 'textarea' :
      e.push (E ('textarea', Object.assign({name, value},attr))); break;

    case 'select' :
      attr.type ='select-one';
    case 'select-multiple' : case 'select-one' :
      e.push (E('select', Object.assign({name, value}, attr), option.map (_=> new Option (..._))));
      break;

    case 'checkbox' : case 'radio' :
      e.push (...(option.map (([a,b,c,d=c])=>
        E('label', attr, [E('input', {name, type, value:b, checked:!!c, defaultChecked:!!d}),a]))) 
      );
      break;
    
    case 'datalist':
      id = 'dl-' + name;
      e.push (
        E('input', Object.assign ({name, value, list:id}, attr)), //type は attr で上書き?
        E('datalist', {id}, option.map(_=> new Option(_)))
      );
      break;
    
    case 'image' :
      e.push (E('input', Object.assign ({name, type, value, src:value}, attr)));
      break;

    
    default :
      e.push (E('input', Object.assign ({name, type, value}, attr)));
      break;
    }

    parent.append (...e);
    return parent;
  }

  
  /*
    フォーム要素群の値をオブジェクト型にして返す
    同じname属性が複数 or select-multipleの場合 or checkbox は配列で返す
  */
  static getValue (root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    //フォームの要素の値を返す
    const V = e=> {
      switch (e.type) {
        case'select-multiple': return [...e.selectedOptions].map(o=>o.value);
        case'checkbox': if(! e.checked) return []; break;
        case'radio': if (! e.checked) return; break;
      }
      return e.value;
    };
  
    //__

    let obj = { };

    for (let e of root.querySelectorAll (query)) {
      let val = V (e);
      let name = e.name;

      if (obj.hasOwnProperty (name)) {
        if (null == val) continue;
        let x = obj[name];
        if (null != x)
          val = [].concat (x, val);
      }

      obj[name] = val;
    }
    return obj;
  }


  /*
  */
  //オブジェクト型の値をフォーム要素群に転置
  static setValue (obj , root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    //要素に値を設置
    const V = (e, val)=> {
      let idx;
      if (Array.isArray (val)) {//配列の場合は、転置ごとにその値を削除する
        switch (e.type) {
          case 'select-multiple' :
            for (let op of e.options) {
              idx = val.indexOf (e.value);
              if (-1 < idx) {
                val.splice (idx, 1);
                e.selected = true;
              } else
                e.selected = false;
            }
            break;
          case 'radio' : case'checkbox' :
            idx = val.indexOf (e.value);
            if (-1 < idx) {
              val.splice (idx, 1);
              e.checked = true;
            } else
              e.checked = false;
            break;
          default :
            e.value = val.shift () || '';
            break;
        }
      }
      else {//転置用の値が単一ならば、複数の同じname属性の要素に同じ値を転置する
        switch (e.type) {
          case 'select-multiple' :
            e.options.forEach (o=> o.selected = o.value == val);
            break;
          case 'radio' : case'checkbox' :
            e.checked = e.value == val;
            break;
          default :
            e.value = val || '';
            break;
        }
      }
    };

    //__

    let
      val = Object.assign ({ }, obj),//copy
      es = root.querySelectorAll (query);

    this.reset ();
    for (let e of es)
      V (e, val[e.name]);
  }

  static reset (root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    let es = root.querySelectorAll (query);
    for (let e of es) {
      switch (e.type) {
        case 'select-one' : case 'select-multiple' :
          for (let op of e.options)
            op.selected = op.defaultSelected;
          break;
        case 'radio' : case 'checkbox' :
          e.checked = e.defaultChecked;
          break;
        default :
          e.value = e.defaultValue || '';
      }
    }
  }
}

$F.mkTable (JSON,T);
$F.setValue ({MEMO: 'abc', TIKU:'中野', GENGO:['PHP','BASIC'], SEIBETU:'woman',ADD:[,2,]});
//alert(8);
$F.reset ();

</script>

ちょっと変更

晴れて自由だ!

2025-03-31
早期退職をした。自由になった。あとどれくらい生きられるだろう?

普段から計画的ではない性格なのだが、春になったらドライブしようと思って車を買っていた。
とても磨きがいのある車。

さぁ、どこへ出かけようか!?

LEXUS LC500 Convertible

*Mini PC と チューナーレスTV とで広告用看板を作る

購入するもの

  1. Mini PC
  2. TV(チューナレス)
  3. 壁固定金具
  4. 適当なHDMIケーブルと電源類(USB TypeC PB を使って必要なボルトの電源を確保できればなお吉)

Mini PC の BIOS を設定して、通電したらPCを自動起動になるように設定する

Aptio Setup - AMI
Boot -> State After G3 -> [S0 State]
他のPCの場合の参照URLhttps://hp.vector.co.jp/authors/VA022629/Manual/PageBIOS.htm

補足

  • S0 PC稼働状態
  • S1 省電力状態
  • S2 省電力状態(CPUへの給電はオフ、チップセット給電はオン)
  • S3 スリープ状態(メモリへは給電されている)
  • S4 休止状態(高速スタートアップを有効にしているときのシャットダウン状態)
  • S5 シャットダウン状態(完全シャットダウン)

Windowsを設定する

  1. とにかく余計なアプリを削除し軽量化する
  2. ブラウザを自動起動になるように設定する (以下ディレクトリを参照しFirefox のショートカットを作成する)

>||C:\Users\????\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup|

  1. Firefox のリンク先の設定

>||"C:\Program Files\Mozilla Firefox\firefox.exe" http://対象となるURL --kiosk|

適当に画面が切り替わるようなプログラムを書いて、サーバにアップする

Mini PC の余った USB端子から 5Vの電源を取り出す

  1. ケーブル&端子付き
  2. LEDライトでも買う
  3. 適当にハンダ付けしてつなぐ

Javascript 関数を繰り返して呼び出す

callBack 関数が true を返す間、定期的に関数を呼び出す

class Repeater {
  
  #cnt = null;
  #timerId = null;

  #loop = function () {
    if (this.#cnt && this.cbFunc (--this.#cnt) && this.#cnt)
      this.#timerId = setTimeout (this.#loop.bind (this), this.interval);
    else
      this.stop ();
  }

  //_______

  constructor (cbFunc, count, interval) {
    this.cbFunc = cbFunc;
    this.count = this.#cnt = count;
    this.interval = interval;
  }

  start (force = false) {
    this.stop ();
    if (force)
      this.#cnt = this.count;
    this.#loop ();
    return this;
  }

  stop (force = false) {
    if (this.#timerId)
      this.#timerId = clearTimeout (this.#timerId) || null;
    if (force)
      this.#cnt = 0;
    return this;
  }

  //_______

  static create (cbFunc, count = 1, interval = 1000) {
    if ('function' !== typeof cbFunc)
      throw new Error ('関数ではありません');

    let obj = new this (cbFunc, count, interval);
    return obj.start ();
  }
}

callBack 関数が true を返す間、定期的に関数を呼び出す。その2
勉強のため promise を使ってみた。

class AsyncRepeater {
  
  #cnt = null;
  #timerId = null;

  #loop = function () {
    if (this.#cnt && this.cbFunc (--this.#cnt) && this.#cnt) //マイナスに対し ―∞ へ
      this.#timerId = setTimeout (this.#loop.bind (this), this.interval);
    else
      this.end ();
  };

  //_______

  constructor (cbFunc, count, interval, resolve, reject) {
    this.cbFunc = cbFunc;
    this.count = this.#cnt = count;
    this.interval = interval;
    this.resolve = resolve;
    this.reject = reject;
  }

  start (force = false) {
    this.stop ();
    if (force)
      this.#cnt = this.count;
    this.#loop ();
    return this;
  }

  restart () {
    this.#loop ();
    return this;
  }

  stop () {//あくまでも中断
    if (this.#timerId)
      this.#timerId = clearTimeout (this.#timerId) || null;
    return this;
  }

  end (force = false) {
    this.stop ();
    if (force && this.reject)//強制中断
      this.reject ();
    else if (this.resolve)
      this.resolve ();
    return this;
  }

  //_______

  static create (cbFunc, count = 1, interval = 1000, async = true) {
    if ('function' !== typeof cbFunc)
      throw new Error ();

    let obj, resolve, reject;
    let p =  async
      ? new Promise ((a, b)=> {resolve = a; reject = b})
      : null;

    obj = new this (cbFunc, count, interval, resolve, reject);
    Object.assign (obj, {promise: p});
    obj.start ();
    return obj;
  }
}

応用して、画面をフェードイン・フェードアウトで切り替えられるチェンジャーを作ってみた
css のアニメーションが楽だったような感じ。
LI 要素に data-change_time を設け、以下のようにセットする
構文は、[フェードインまでの時間, 表示し続ける時間, フェードアウトまでの時間, 空白の時間] とする
停止・再開機能を付けようと思ったが中止。

フリーソフトの exeUSB なるものを見つけた。
USBメモリを装着すると、自動で指定のアプリを起動できる
そこで Chrome --kiosk のモードで起動すれば、電子看板のように使える

html

<ul id="SAMPLE">
  <li data-change_time="1000,1500,1000,1000"> 
  <li ...>
  ...

Script

DisplayChanger.create (SAMPLE);

class DisplayChanger {

  #skip = function (n = 1) { // - !?
    for (let i = 0, I = n % this.children.length; i < I; i++)
      this.parent.appendChild (this.current);
  };

  #getStateTimes (e = this.current) {
    let state = /^(?:\s*([+-]?\d*)(?:,\s*([+-]?\d*)(?:,\s*([+-]?\d*)(?:,\s*([+-]?\d*)?)?)?)?)?(?:,.*)?$/.exec (e.dataset.change_time);
    if (null == state) console.error (e, e.dataset.change_time);
    return state.slice (1).map ((a, i, _, val = parseInt (a))=> isNaN (val) ? this.default[i]: val);
  };

  #next = function () {
    this.#skip ();
    this.#loop ();
  };

  #loop = async function () {
    let e = this.current;
    let [t0, t1, t2, t3] = this.#getStateTimes (e);
    let i = this.interval;

    await (AsyncRepeater.create (this.#fadeIn (e, t0, i), -1, i)).promise;
    await this.#keep (t1);
    await (AsyncRepeater.create (this.#fadeOut (e, t2, i), -1, i)).promise;
    await this.#keep (t3);

    this.#next ();
  };

  #keep = function (a) {
    return new Promise (c=> setInterval (c, a));
  }

  #fadeIn = function (e, time, interval) {
    let max = Math.round (time / interval);
    let step = 1 / max;
    let opacity = 0;

    return function () {
      opacity += step;
      e.style.opacity = (1 < opacity) ? 1: opacity;
      return opacity < 1;
    };
  }

  #fadeOut = function (e, time, interval) {
    let max = Math.round (time / interval);
    let step = 1 / max;
    let opacity = 1;

    return function () {
      opacity -= step;
      e.style.opacity = (opacity < 0) ? 0: opacity;
      return 0 < opacity;
    };
  }


  //_____

  constructor (a, b, c, d) {
    this.parent   = a;
    this.begin    = b;
    this.interval = c;
    this.default  = d;//[fadeInTime, displayTime, fadeOutTime, blankTime]
    this.#skip (b);
    this.isInOpration = false; //作動中か?
  }

  start () {
    this.isInOpration = true;
    this.#loop ();
  }

  //_____

  get current () {
    return this.parent.firstElementChild;
  }

  get children () {
    return [...this.parent.children];
  }

  //_____
  
  static create (parent, begin = 0, interval = 20, fadeInTime = 1000, displayTime = 5000, fadeOutTime = 500, blankTime = 300) {
    if (1 > arguments.length)
      throw new Error ('引数がありません');

    [...parent.children].forEach (e=> e.style.opacity = 0);

    let tm = parent.dataset;
    interval   = Math.max (1, parseFloat (tm.interval || interval));
    fadeInTime = Math.max (1, parseFloat (tm.fadeInTime || fadeInTime));
    displayTime= Math.max (1, parseFloat (tm.displayTime || displayTime));
    fadeOutTime= Math.max (1, parseFloat (tm.fadeOutTime || fadeOutTime));
    blankTime  = Math.max (1, parseFloat (tm.blankTime || blankTime));

    let obj = new this (parent, begin, interval, [fadeInTime, displayTime, fadeOutTime, blankTime]);
    obj.start ();
    return obj;
  }
}

配列からテーブルを作る2

配列からテーブルを作る2

配列からテーブルを作る場合、セルを結合したりclassName を指定したい時がある。そこで配列のセルに該当するテキストから colSpan , rowSpan などを取り出せるようにした。

  1. 文字列の先頭が "#" ならば TH 要素になる
  2. ”!r4c2 def" ならば rowSpan=4, colspan=2, textContent = "def" となる
  3. "!{"className":"red"} ghi ならば class="red", textContent = "ghi" となる。 { } で囲まれる部分は、JSON 形式に変換されるのでダブルクォーテーションを適材適所に使用すること。また {"innerHTML":"<a href='xxx'>Test</a>"}とすれば子要素以降も文字列から生成できる。その場合は適切に文字をエスケープすること。

let ary = [
  ["#!r4{"className":"red"} abc", "#!c4 def, ... ],
  [...],
  ... 
]
function ary2thead (ary = [ ], thead = document.createElement ('thead')) {
  for (let row of ary)
    thead.insertRow().append(...row.map(cell=>{
      let [, th, rowSpan, colSpan, json, textContent = ''] = (new RegExp ('^(#)?(?:!(?=[rc\\{])(?:r([2-9]|[1-9]\\d+)|r[01]|r0\\d+)?(?:c([2-9]|[1-9]\\d+)|c[01]|c0\\d+)?({.+})?)?\\s*(.*)\\s*$','g')).exec (cell) ?? [ ];
      return Object.assign (document.createElement (th ? 'th': 'td'),  Object.entries ({rowSpan, colSpan, textContent}).reduce((a,[b,c])=>((c===undefined||(a[b]=c)),a), {}), json ? JSON.parse(json): {});
    }));
  return thead;
}

短く

const
  ary2thead= (a=[],b=document.createElement('thead'))=>{
  for(let c of a)b.insertRow().append(...c.map(d=>{
  let[,e,f,g,h,i='']=(new RegExp('^(#)?(?:!(?=[rc\\{])(?:r([2-9]|[1-9]\\d+)|r[01]|r0\\d+)?(?:c([2-9]|[1-9]\\d+)|c[01]|c0\\d+)?({.+})?)?\\s*(.*)\\s*$','g')).exec(d)??[];
  return Object.assign(document.createElement(e?'th':'td'),Object.entries({rowSpan:f,colSpan:g,textContent:i}).reduce((j,[k,l])=>((l===undefined||(j[k]=l)),j),{}),h?JSON.parse(h):{});
  }));return b};

文字列からテーブルを作る

配列からテーブルを作る

<!DOCTYPE html>
<meta charset="utf-8">
<title></title>

<style>
  .red { color: red;}
</style>

<body>
<table border="1" id="TB"></table>


<script>


function ary2thead (ary = [ ], thead = document.createElement ('thead')) {

  const
    textSplitStr = (
        th = '(#)?', //文字列の先頭が "#" なら th要素とするための判別
        rowSpan = '(?:r([2-9]|[1-9][0-9]+)|r[01]|r0[0-9]+)?',//rowSpan の2以上を対象として取得
        colSpan = '(?:c([2-9]|[1-9][0-9]+)|c[01]|c0[0-9]+)?',//colSpan の2以上を対象として取得
        attr = '(?:\\{(.+)?\\})?',//attribute として { ... } 括弧の内側を文字列として取得
        text = '\\s*(.*)\\s*', //前後の空白を取り除いて取得
        ctrlCom = '(?:!(?=[rc\\{])'+ rowSpan + colSpan + attr + ')?'
      )=> '^' + th + ctrlCom + text + '$',
    
    attrSplitStr = '\\s*(.+?)\\s*\\:\\s*(.*?)\\s*(?:,|;|$)',
    rows = [ ];


  for (let row of ary) {
    let tds = [];
    for (let cell of row) {
      let
        splitter = new RegExp (textSplitStr (), 'gi');
        [, th, rowSpan, colSpan, attr, textContent = ''] = splitter.exec (cell) ?? [ ],
        prop = new Map;

      if (rowSpan)
        prop.set ('rowSpan', rowSpan);
      if (colSpan)
        prop.set ('colSpan', colSpan);
      
      if (attr)
        for (let a, splitter = new RegExp (attrSplitStr, 'gi'); a = splitter.exec (attr);  )
            prop.set (a[1],a[2]);

      prop.set ('textContent', textContent);

      tds.push (Object.assign (document.createElement (th ? 'th': 'td'), Object.fromEntries (prop)));
    }
    rows.push (tds);
  }

  rows.reduce ((a, b)=>(a.insertRow ().append (...b), a), thead);
  return thead;
}


const RECS = `

#!R2C1 No	#!r2 Name	#!r1c2 INFO
#Age	#ETC

#1	akio	57	abc
#!{id:sss,className: red}2	ayako	56	def



 `;

const csv2ary = csv => csv.split (/r\n|\r|\n/g).reduce ((a,b)=>b.trim()?[...a,b]:a,[]).map (row=> row.split (/\t/g));

ary2thead (csv2ary (RECS), TB);

</script>

ショートコーディングしてみる

<!DOCTYPE html>
<meta charset="utf-8">
<title></title>

<style>
  .red { color: red;}
</style>

<body>
<table border="1" id="TB"></table>


<script>


function ary2tbody (recs = [ ], tbody = document.createElement ('tbody')) {
  function* splitter(a,b=new RegExp(attrSplitStr,'g'),c){while(c=b.exec(a))yield c.slice(1)}
  const
    addObj = (a,[b,c])=>(a[b]=c,a),
    delObj = (a,[b,c])=>((undefined===c)&&delete a[b],a),
    makeTD = (a)=>{
      let
        [, th, rowSpan, colSpan, attr, textContent] = (new RegExp (textSplitStr, 'i')).exec (a) ?? [ ],//文字列を分解
      prop = [...splitter (attr)].reduce (addObj, { rowSpan, colSpan, textContent });//要素の属性

      return Object.assign (
        document.createElement (th ? 'th': 'td'), //フラグとしての th により生成
        Object.entries (prop).reduce (delObj, prop) // prop オブジェクトから値が無いものは削除する 
      )
    },
    attrSplitStr = '\\s*(.+?)\\s*\\:\\s*(.*?)\\s*(?:,|;|$)',//文字列から属性値を切り出す "key:value, key2:value2, ..."
    textSplitStr = //#!r2c3{className:red} text
    '^' + '(#)?' + //th 文字列の先頭が "#" なら th要素とするための判別
    '(?:!(?=[rc{])'+ //制御文字は !で始まり以下の順で記述する
        '(?:r([2-9]|[1-9][0-9]+)|r[01]|r0[0-9]+)?' + //rowSpan の2以上を対象として取得
      '(?:c([2-9]|[1-9][0-9]+)|c[01]|c0[0-9]+)?' + //colSpan の2以上を対象として取得
      '(?:\\{(.+)?\\})?' + //{attr: 123, attr2: 456} の用に属性を取得する
    ')?' +
    '\\s*(.*)\\s*' + '$';//textContent 前後の空白を取り除いて取得

  recs.forEach (row=> tbody.insertRow ().append (...row.map (makeTD)));
  return tbody;
}


const RECS = `

#!R2C1 No  #!r2 Name  #!r1c2 INFO
#Age  #ETC

#1  akio  57  abc
#!{id:sss,className: red}2  ayako  56  def



 `;

const csv2ary = csv => csv.split (/r\n|\r|\n/g).reduce ((a,b)=>b.trim()?[...a,b]:a,[]).map (row=> row.split (/\t/g));

ary2thead (csv2ary (RECS), TB);

</script>

さらにショート!

<!DOCTYPE html>
<meta charset="utf-8">
<title></title>

<style>
  .red { color: red;}
</style>

<body>
<table border="1" id="TB"></table>


<script>
const ary2tbody=(a=[],b=document.createElement('tbody'))=>{
function*c(a,b=new RegExp('\\s*(.+?)\\s*\\:\\s*(.*?)\\s*(?:,|;|$)','g'),c){while(c=b.exec(a))yield c.slice(1)}
const d=(a,[b,c])=>(a[b]=c,a),e=(a,[b,c])=>((undefined===c)&&delete a[b],a),f=g=>{let[,h,i,j,k,l]=(new RegExp('^(#)?(?:!(?=[rc{])(?:r([2-9]|[1-9][0-9]+)|r[01]|r0[0-9]+)?(?:c([2-9]|[1-9][0-9]+)|c[01]|c0[0-9]+)?(?:\\{(.+)?\\})?)?\\s*(.*)\\s*$','i')).exec(g)??[],m=[...c(k)].reduce(d,{rowSpan:i,colSpan:j,textContent:l});return Object.assign(document.createElement(h?'th':'td'),Object.entries(m).reduce(e,m))};
a.forEach(c=>b.insertRow().append(...c.map(f)));return b}


const RECS = `

#!R2C1 No	#!r2 Name	#!r1c2 INFO
#Age	#ETC

#1	akio	57	abc
#!{id:sss,className: red}2	ayako	56	def



 `;

 const csv2ary = csv => csv.split (/r\n|\r|\n/g).reduce ((a,b)=>b.trim()?[...a,b]:a,[]).map (row=> row.split (/\t/g));

ary2tbody (csv2ary (RECS), TB);


</script>

再考して更に短く

const
  ary2tbody=(a=[],b=document.createElement('tbody'))=>{
  let A='^(#)?(?:!(?=[rc\\{])(?:r([2-9]|[1-9]\\d+)|r[01]|r0\\d+)?(?:c([2-9]|[1-9]\\d+)|c[01]|c0\\d+)?(?:\\{(.+)?\\})?)?\\s*(.*)\\s*$',B=RegExp,C=Object;  
  a.map(c=>{b.insertRow().append(...c.map(d=>{let[,z,y,x,w,v]=(new B(A,'i')).exec(d)??[],u={rowSpan:y,colSpan:x,textContent:v};
  C.entries(u).map(a=>a[1]===undefined&&delete u[a[0]]);for(let a,b=new B('\\s*(.+?)\\s*\\:\\s*(.*?)\\s*(,|;|$)','g');a=b.exec(w);u[a[1]]=a[2]);
  return C.assign(document.createElement(z?'th':'td'),u)}))});return b};