班固米-条目职位自定义排序与折叠

对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置

目前為 2024-11-16 提交的版本,檢視 最新版本

// ==UserScript==
// @name         班固米-条目职位自定义排序与折叠
// @namespace    https://github.com/weiduhuo/scripts
// @version      1.2.0-1.0
// @description  对[动画]条目的制作人员信息进行职位的自定义排序与折叠,可在[设置-隐私]页面进行相关设置
// @author       weiduhuo
// @match        *://bgm.tv/subject/*
// @match        *://bgm.tv/settings/privacy
// @match        *://bangumi.tv/subject/*
// @match        *://bangumi.tv/settings/privacy
// @match        *://chii.in/subject/*
// @match        *://chii.in/settings/privacy
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';
  const SCRIPT_NAME = '班固米-职位排序组件';

  /* 职位的排序列表 jobOrder 与默认折叠的职位 foldableJobs 的合并信息
   * 数据类型的标准样式:
   * Array[ String | RegExp | Array[Boolean, String | RegExp, ...]]
   * 其中 Boolean 表示子序列内的职位是否默认折叠,缺损值为 False,需位于子序列的首位才有意义
   * (其中 `,,` 表示插入 null 元素,用于输出格式化文本时标记换行 )
   */
  const staffMapList = [,
    "中文名", "类型", "适合年龄", /地区/, "语言", "对白", "话数", "总话数", [true, "季数"], ,
    "放送开始", "开始", "放送星期", "放送时间", "上映年度", /上映/, "发售日", "片长", /片长/, ,
    ,
    "原作", "原案", "人物原案", "原作插图", [true, "原作协力"], ,
    "团长", "总导演", "导演", "副导演", "执行导演", "主任导演", "联合导演", "系列监督", ,
    "系列构成", "脚本", "编剧", [true, /脚本|内容|故事|文艺|主笔/], ,
    "分镜", "OP・ED 分镜", "主演出", "演出", [true, "演出助理"], ,
    "人物设定", ,
    ,
    "总作画监督", [false, "作画监督"], [true, "作画监督助理"], "动作作画监督", "机械作画监督", "特效作画监督", /.*作画.*(监|导)/, ,
    "主动画师", "主要动画师", [true, "构图"], [true, "原画"], [true, "第二原画", "补间动画"], "数码绘图", /(原画|动画|動画)(?!制|检查)/, ,
    "动画检查", [true, /动画检查/], ,
    ,
    "设定", "背景设定", "道具设计", /(?<!色彩|美术|美術)(设|設)(?!.*制)/, /Design|デザイン|Motion Graphic|モーショングラフィック/, ,
    "色彩设计", [false, "色彩指定", "色指定"], [true, "上色", /(?<!角)色/], ,
    "美术监督", /(美术|美術).*导演/, [false, "美术设计"], "概念美术", "视觉概念", "概念艺术", ,
    [false, "背景美术"], [true, /景/], /ART WORK|美术|美術|艺术|工艺|创意|绘制/, ,
    ,
    /CG.*导演/, "3DCG", [false, /2D|3D|CG|コンピュータ/], "模型监督", "绑骨监督", [/建模|模型|动作|表情|骨/], ,
    "摄影监督", "副摄影监督", "后期监督", [true, "摄影", "特效", "照明", "特技", /摄影|モニター|特效|动效|合成|拍|Effect|技术/], ,
    "現像", /タイトル|标题|字幕/, ,
    [false, "剪辑", "编集"], [true, /(?<!音.*)剪辑/], "编辑", [true, "场记"], ,
    "监修", /监修|監修/, "顾问", /顾问/, ,
    ,
    "音响监督", [true, "音响", "音响制作", "音效", "拟音", /音响/], "录音", [true, "录音助理", "混声", /录音|声/], ,
    "配音", "主演", "キャスティング", [true, /配音|((?<!歌)演出)/], ,
    "音乐", "音乐制作", [true, "音乐制作人", "音乐助理"], [true, /音/], ,
    "主题歌演出", [true, "主题歌作词", "主题歌作曲", "主题歌编曲"], "插入歌演出", ,
    [true, "插入歌作词", "插入歌作曲", "插入歌编曲"], [true, "选曲"], /曲|歌/, ,
    ,
    "企画", [true, "企画协力"], "企划制作人", /企画|企划|出品|策划/, "监制", /监制/, ,
    "执行制片人", "总制片人", "制片人", "总制片", "制片", [true, "副制片人", "联合制片人", "助理制片人", /(?<!动画|動画)制片/], ,
    [true, /行政|审/, "责任编辑"], [true, /法务/], [true, "宣传", /宣传|宣伝|広報/], /市场|运营|衍生/, ,
    "制作", "製作", [true, "制作著作"], ,
    "动画制片人", [true, "制作管理", "制作统筹", "制作主任", "制作助理"], ,
    [true, "设定制作"], [true, "计划管理", "制作进行", "制作进行协力"], ,
    "制作协调", "制作协力", "制作助手", "协力", /协力|協力/, [true, /取材/], [true, "特别鸣谢", /鸣谢|Thanks/], ,
    ,
    "动画制作", [true, /制作|製作/], ,
    "别名", /.+名$/, ,
    "发行", "官方网站", "在线播放平台", "链接", "播放电视台", "其他电视台", "配给", /配(?!音)|連載|番組|版|播放(?!结束)/, ,
    "播放结束", "结束", ,
    ,
    "其他", /其他/, ,
    "===此处插入未被匹配的职位===", ,
    "Copyright",
  ];
  // 职位的排序列表
  const jobOrder = [];
  // 默认折叠的职位
  const foldableJobs = [];
  // 默认值的格式化文本的缓存
  let mapListTextBuffer = null;
  // 是否对职位信息进行了折叠 (依此判断 `更多制作人员` 开关的必要性)
  let hasFolded = false;

  if (location.pathname.startsWith('/subject/')) {
    trySortStaff();
  }
  else if (location.pathname === '/settings/privacy') {
    const ui = buildSettingUI({ id: 'staff-sorting' });
    document.getElementById('columnA').appendChild(ui);
    if (location.hash.slice(1) === 'staff-sorting') {
      ui.scrollIntoView({ behavior: 'smooth' });
    }
  }

  function trySortStaff() {
    isTargetMediaType().then(result => {
      if (!result) {
        // console.log(`${SCRIPT_NAME}:此页面条目不是动画类别,脚本不会继续执行。`);
        return;
      } else {
        loadMapList();
        if (staffMapList.length) {
          sortStaff();
        } else {
          addFoldableTag();
          console.log(`${SCRIPT_NAME}:自定义 staffMapList 数值为空,不进行职位排序`)
        }
        changeToToggleButton();
      }
    }).catch(error => {
      console.error(`${SCRIPT_NAME}:API查询错误 - ${error}`);
    });
  }

  function sortStaff() {
    // 职位信息字典
    const staffDict = getStaffDict();

    // 清空原始的 staff 列表
    const ul = document.querySelector('#infobox');
    ul.innerHTML = '';

    // 未能匹配职位的待插入位置
    let liAfterIntsert = null;
    let insterTag = false;
    let insertFold = false;

    // 按照预定顺序添加到 DOM
    jobOrder.forEach(item => {
      const matchingRoles = [];
      // 1.正则匹配
      if (item instanceof RegExp) {
        matchingRoles.push(...Object.keys(staffDict).filter(key => item.test(key)));
        if (matchingRoles.length) {
          console.log(`${SCRIPT_NAME}:使用正则表达式 "${item}" 成功匹配 \{${matchingRoles}\}`);
        } else return;
      } else if (typeof item === 'string') {
        // 2.键值匹配
        if (item && item in staffDict) {
          matchingRoles.push(item);
        // 3.特殊关键字处理
        } else if (item.startsWith('==')) {
          // 激活待插入位置
          insterTag = true;
          insertFold = foldableJobs.includes(item);
          console.log(`${SCRIPT_NAME}:激活将未能匹配职位插入指定位置`)
        } else return
      // 4.其余情形均忽略(且对于意外类型不报错)
      } else return;

      // 添加职位,并判断是否默认折叠
      matchingRoles.forEach(role => {
        const li = document.createElement('li');
        li.innerHTML = staffDict[role];
        if (typeof item === 'string' && foldableJobs.includes(role)
          || item instanceof RegExp && foldableJobs.includes(item)) {
          li.classList.add('folded', 'foldable');
          if (!hasFolded) hasFolded = true;
        }
        ul.appendChild(li);
        delete staffDict[role]; // 从字典中删除已处理的职位
        // 保存待插入位置
        if (insterTag) {
          liAfterIntsert = li;
          insterTag = false;
        }
      });
    });

    // 将剩余的职位(不在排序列表中的)按原顺序添加到待插入位置
    Object.keys(staffDict).forEach(role => {
      const li = document.createElement('li');
      li.innerHTML = staffDict[role];
      if (insertFold) li.classList.add('folded', 'foldable');
      if (liAfterIntsert) ul.insertBefore(li, liAfterIntsert);
      // 未设置待插入位置,则默认插入到末尾,且默认不折叠
      else ul.appendChild(li);
    });
    console.log(`${SCRIPT_NAME}:未能匹配到的职位 ${JSON.stringify(staffDict, null, 2)}`);
  }

  async function isTargetMediaType() {
    const smallTag = document.querySelector('h1.nameSingle > small.grey');
    if (smallTag) {
      // 优先通过网页内容判断
      const text = smallTag.innerText.trim();
      return ['TV', 'WEB', '剧场版', 'OVA'].includes(text);
    } else {
      // 通过API查询
      const urlParts = location.href.split('/');
      const subjectID = urlParts[urlParts.length - 1];
      const response = await fetch(`https://api.bgm.tv/v0/subjects/${subjectID}`);
      const subject = await response.json();
      return subject.type === 2; // 判断是否为动画类别
    }
  }

  // 获取一个字典来存储网页中的职位信息
  function getStaffDict() {
    const staffDict = {};
    const lis = document.querySelectorAll('#infobox > li');
    lis.forEach(li => {
      const tip = li.querySelector('span.tip');
      if (tip) {
        const role = tip.innerText.trim().slice(0, -1); // 去掉最后的冒号
        staffDict[role] = li.innerHTML;
      }
    });
    return staffDict;
  }

  // 为网页原有的 `folded` 类别添加 `foldable` 便签,用于实现切换
  function addFoldableTag() {
    const lis = document.querySelectorAll('#infobox > li');
    lis.forEach(li => {
      if (li.classList.contains('folded')) {
        li.classList.add('foldable');
        if (!hasFolded) hasFolded = true;
      }
    });
  }

  /* 将原本存在的 `更多制作人员` 一次性按钮,转绑新事件,并改为永久性开关
   * 使用网页原有的 `folded` 元素类别,实现对立于 sortStaff 功能
   * 添加不存在的 `更多制作人员` 按钮,否则一些职位信息将不可见
    <div class="infobox_expand">
      <a href="javascript:void(0)">更多制作人员 +</a>
      <!-- href 属性需保留,不然鼠标悬浮效果会失效 -->
    </div>
   */
  function changeToToggleButton() {
    const buttonValue = { on: '更多制作人员 +', off: '更多制作人员 -' };
    const parent = document.querySelector('.infobox_container');
    let moreLink = parent.querySelector('.infobox_expand a');
    if (!hasFolded) {
      // 无必要,不进行事件绑定与可能的添加,并将原有的开关隐藏
      if (moreLink) {
        moreLink.style.display = 'none';
        console.log(`${SCRIPT_NAME} - 将原有的 '${buttonValue.on}' 隐藏`);
      }
      return;
    }
    if (!moreLink) {
      moreLink = createElement('a', { href: 'javascript:void(0)' }, buttonValue.on);
      const expand = createElement('div', { class: 'infobox_expand' }, [moreLink]);
      parent.appendChild(expand);
      console.log(`${SCRIPT_NAME}:添加原不存在的 '${buttonValue.on}' 按钮`);
    }
    moreLink.addEventListener('click', function (event) {
      event.stopImmediatePropagation(); // 阻止其他事件的触发
      const foldedLis = document.querySelectorAll('.foldable');
      const isHidden = moreLink.innerText == buttonValue.on;
      foldedLis.forEach(li => {
        if (isHidden) {
          li.classList.remove('folded');
        } else {
          li.classList.add('folded');
        }
      });
      moreLink.innerText = isHidden ? buttonValue.off : buttonValue.on;
    }, { capture: true }); // 使事件处理函数在捕获阶段运行
  }

  /* 创建用户设置 UI 界面
   * 仿照 #columnA 中的同类元素进行构建,使用原有的结构与样式
    <table class="settings" style="margin-left: 5px">
      <colgroup>
        <col style="width: 90%" />
        <!-- 右侧留出的空间,是为了移动端可以同时选择滑动 textarea Or mainpage -->
        <col />
      </colgroup>
      <tbody>
        <tr>
          <td colspan="2">
            <h2 class="subtitle">条目职位排序 · 默认折叠的职位</h2>
          </td>
        </tr>
        <!-- 此处添加子模块 -->
      </tbody>
    </table>
   */
  function buildSettingUI(mainStyle) {
    const mainTitle = createElement('tr', null, [
      createElement('td', { colSpan: '2' }, [
        createElement('h2', { class: 'subtitle' }, '条目职位排序 · 默认折叠的职位')
      ])
    ]);
    const animeBlock = buildAnimeBlock();
    const ui = createElement('div', mainStyle, [
      createElement('table',
        { class: 'settings', style: { marginLeft: '5px' } }, [
        createElement('colgroup', null, [
          createElement('col', { style: { width: '90%' } }),
          createElement('col'),
        ]),
        createElement('tbody', null, [
          mainTitle, animeBlock
          // 可拓展其他类型条目的模块
        ])
      ])
    ]);
    return ui;
  }

  /* 创建 staffMapList 文本内容编辑界面
   * 对于 textarea, button 等控件仍然使用原有的结构与样式
    <tr>
      <td>
        <h2 class="subtitle">动画条目</h2>
        <div>
          <div class="markItUp">
            <textarea class="quick markItUpEditor hasEditor codeHighlight"
              id="staff_map_list" name="staff_map_list" style="line-height: 21px">
              <!-- staffMapListText -->
            </textarea>
          </div>
          <input class="inputBtn" type="submit" name="submit_context" value="保存" style="margin-right: 5px" />
          <input class="inputBtn" type="submit" name="reset_context" value="恢复默认" style="margin-right: 10px" />
          <p class="tip_j" style="display: none"><!-- message --></p>
        </div>
      </td>
      <td><!-- 为移动端预留的 mainpage 滑动空间 --></td>
    </tr>
  */
  function buildAnimeBlock() {
    // 搭建标题
    const subTitle = createElement('h2', { class: 'subtitle' }, '动画条目');
    // 搭建简易提示框
    const msgCntr = createElement('p', { class: 'tip_j', style: { display: 'none' } });

    // 搭建文本框
    let hasInputted = false;
    let {text, isDefault} = getMapListText(false);
    if (isDefault) setMessage(msgCntr, '现为默认设置'); // 初始化时,提醒用户已为默认设置
    const textArea = createElement('textarea', {
      class: 'quick markItUpEditor hasEditor codeHighlight', id: 'staff_map_list', name: 'staff_map_list',
      style: { fontSize: '13x', lineHeight: '21px' }
    },
      text, {
        input: () => {
          if (!hasInputted) hasInputted = true;
          if (isDefault) isDefault = false;
          // console.log("IS INPUTTING");
        }
    });
    // 搭建提交按钮
    const submitBtn = createElement('input',
      { class: 'inputBtn', type: 'submit', name: 'submit_context', value: '保存', style: { marginRight: '5px' } }, null, {
      click: () => {
        // 判断是否为重置后未对默认内容进行修改
        if (isDefault && !hasInputted) {
          resetMapList();
          setMessage(msgCntr, '保存成功!恢复默认设置');
          // 恢复初始状态
          hasInputted = false;
          return;
        }
        const [modifiedData, isModified] = modifyMapListJSON(textArea.value);
        // 强制将用户输入的文本外层嵌套 `[]`,若为重复嵌套可在 loadMapList 中识别并去除
        const savedDate = `[${modifiedData}]`;
        const parsedData = parseMapListJSON(savedDate);
        if (parsedData) {
          // 保存数据
          saveMapListText(savedDate);
          // 页面显示
          if (isModified) trySetText(textArea, msgCntr, modifiedData, '保存成功!并自动纠错', true);
          else setMessage(msgCntr, '保存成功!');
        } else setMessage(msgCntr, '保存失败!格式存在错误');
        // 恢复初始状态
        hasInputted = false;
      }
    });
    // 搭建重置按钮
    const resetBtn = createElement('input',
      { class: 'inputBtn', type: 'submit', name: 'reset_context', value: '恢复默认', style: { marginRight: '10px' } }, null, {
      click: async () => {
        if (isDefault) {
          setMessage(msgCntr, '已为默认内容');
          return;
        }
        await trySetText(textArea, msgCntr, getMapListText(true).text, '已恢复默认内容', false);
        // 需进行同步等待,由于 setText 可能会触发 input 事件
        isDefault = true;
        hasInputted = false;
      }
    });

    // 搭建外部结构
    const textCntr = createElement('div', { class: 'markItUp' }, [textArea]);
    const animeBlock = createElement('tr', null, [
      createElement('td', null, [
        subTitle,
        // 可拓展折叠效果
        createElement('div', null, [textCntr, submitBtn, resetBtn, msgCntr])
      ]),
      createElement('td')
    ]);
    return animeBlock;
  }

  /* 优先尝试使用 execCommand 方法改写文本框,使得改写前的用户历史记录不被浏览器清除
   * (虽然 execCommand 方法已被弃用...但仍然是实现该功能最便捷的途径)
   */
  async function trySetText(textArea, msgCntr, text, msg, isRestore, transTime = 100) {
    let [scrollVert, scrollHoriz, cursorPos] = savePos();
    try {
      setMessage(msgCntr);
      await clearAndSetTextarea(textArea, text, transTime);
      setMessage(msgCntr, `${msg},可快捷键撤销`, 0);
    } catch (e) {
      textArea.value = '';
      await new Promise(resolve => setTimeout(resolve, transTime));
      textArea.value = text;
      setMessage(msgCntr, msg, 0);
      console.log(`${SCRIPT_NAME}:浏览器不支持 execCommand 方法,改为直接重置文本框,将无法通过快捷键撤销重置`)
    }
    if (isRestore) restorePos();

    // 保存滚动位置和光标位置
    function savePos() {
      return [textArea.scrollTop, textArea.scrollLeft, textArea.selectionStart];
    }
    // 恢复滚动位置和光标位置
    function restorePos() {
      const currentTextLen = textArea.value.length;
      if (cursorPos > currentTextLen) cursorPos = currentTextLen;
      textArea.scrollTop = Math.min(scrollVert, textArea.scrollHeight);
      // textArea.scrollLeft = Math.min(scrollHoriz, textArea.scrollWidth - textArea.clientWidth);
      textArea.setSelectionRange(cursorPos, cursorPos);
    }
  }

  async function clearAndSetTextarea(textarea, newText, timeout = 100) {
    textarea.focus();
    // 全选文本框内容并删除
    textarea.setSelectionRange(0, textarea.value.length);
    document.execCommand('delete');
    // 延迟一段时间后,插入新的内容
    await new Promise(resolve => setTimeout(resolve, timeout));
    document.execCommand('insertText', false, newText);
  }

  async function setMessage(container, message, timeout = 100) {
    container.style.display = 'none';
    if (!message) return; // 无信息输入,则隐藏
    // 隐藏一段时间后,展现新内容
    if (timeout) await new Promise(resolve => setTimeout(resolve, timeout));
    container.textContent = message;
    container.style.display = 'inline';
  }

  function loadMapList() {
    // 读取可能的非默认设置
    let jsonString = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    if (jsonString) {
      let parsedData = parseMapListJSON(jsonString);
      if (parsedData) {
        // 修复外层重复嵌套 `[]` 的形式 (忽略存在的漏洞,形如:[[true, ["a"], "b"]] )
        if (parsedData.length === 1 && Array.isArray(parsedData[0]) && typeof parsedData[0][0] !== 'boolean') {
          parsedData = parsedData[0];
        }
        staffMapList.length = 0;
        staffMapList.push(...parsedData);
      } else console.log(`${SCRIPT_NAME}:自定义 staffMapList 解析失败,将使用脚本默认的数据`);
    }
    // 将数据拆解为 jobOrder 与 foldableJobs
    staffMapList.forEach(item => {
      if (Array.isArray(item) && item.length) {
        // 对数组进行完全展平,提高对非标多层数组的兼容性
        item = item.flat(Infinity);
        // 对于标准样式,仅当 Boolean 为一级子序列的首元素时,对该子序列的全部元素生效
        // 此时更广义的表述为,仅当 Boolean 为一级子序列的最左节点时,对该子序列的全部元素生效
        if (typeof item[0] === 'boolean') {
          if (item[0]) foldableJobs.push(...item.slice(1));
          jobOrder.push(...item.slice(1));
        } else {
          jobOrder.push(...item);
        }
      } else if (typeof item !== 'undefined') {
        jobOrder.push(item);
      }
    });
  }

  function resetMapList() {
    localStorage.removeItem('BangumiStaffSorting_animeStaffMapList');
    console.log(`${SCRIPT_NAME}:删除自定义 staffMapList 数据,恢复默认设置`)
  }

  function saveMapListText(jsonStr) {
    localStorage.setItem('BangumiStaffSorting_animeStaffMapList', jsonStr);
    console.log(jsonStr);
    console.log(`${SCRIPT_NAME}:保存自定义 staffMapList 数据`);
  }

  function getMapListText(useDefault) {
    let jsonStr = null;
    if (!useDefault) {
      jsonStr = localStorage.getItem('BangumiStaffSorting_animeStaffMapList');
    }
    const isDefault = jsonStr === null;
    if (jsonStr) {
      jsonStr = jsonStr.slice(1, -1); // 消除首尾的 `[]`
    } else if (mapListTextBuffer) {
      jsonStr = mapListTextBuffer;
    } else {
      // 将默认数据转化为格式化文本
      jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
        /(null,\n )|(\n\s+)/g, (match, g1, g2) => {
          if (g1) return '\n';
          if (g2) return ' ';
          return match;
        }).slice(3, -2); // 消除首部 `[ \n` 与尾部 `\n]`
      // 使得 `[ `->`[` 同时 ` ]`->`]`
      // jsonStr = JSON.stringify(staffMapList, regexReplacer, 1).replace(
      //   /(null,)|(?<!\[)(\n\s+)(?!])|(\[\s+)|(\s+],)/g, (match, g1, g2, g3, g4) => {
      //   if (g1) return '\n';
      //   if (g2) return ' ';
      //   if (g3) return '[';
      //   if (g4) return '],';
      //   return match;
      // }).slice(3, -2);
      mapListTextBuffer = jsonStr;
    }
    return {text: jsonStr, isDefault: isDefault};
  }

  /* 对用户输入可能的常见语法与格式错误,进行自动纠错,以满足 JSON 格式
   * 已基本兼容 JS 格式的文本数据,实现格式转化
   * group2 与 group4 致使正则表达式中不允许出现 [/'"] 三种字符
   */
  function modifyMapListJSON(text) {
    let flags = new Array(10).fill(false);
    const rslt = text.replace(
      /(,\s*(?=]|$))|(,\s*)+(?=,)|(')|(?<!'|")(\/[^\/'"]+\/[gimsuy]*)(?!'|")/g, (match, g1, g2, g3, g4) => {
        isTriggered(0, '删除序列末尾元素后的 `,` 逗号', g1);
        isTriggered(2, '删除连续重复的 `,` 逗号', g2);
        isTriggered(1, '将单引号替换为双引号', g3);
        isTriggered(3, '将正则表达式以双引号包裹', g4);
        if (g1 || g2) return '';
        if (g3) return '"';
        if (g4) return `"${match}"`;
        return match;
      });
    return [rslt, booleanOr(...flags)];

    function isTriggered(index, msg, ...groups) {
      if (!flags[index] && booleanOr(...groups)) {
        console.log(`${SCRIPT_NAME}:触发自动纠错 - ${msg}`);
        flags[index] = true;
      }
    }
    function booleanOr(...values) {
      return values.reduce((acc, val) => acc || val, false);
    }
  }

  /* 初步解析 staffMapList JSON 字符串
   * 仅检查:
   *   1.是否满足 JSON 格式
   *   2.是否为数组类型
   *   3.字符串样式的正则表达式,是否满足规定格式
   * 更进一步的解析,将在 loadMapList 中进行
   */
  function parseMapListJSON(text) {
    let parsedData;
    try {
      parsedData = JSON.parse(text, regexReviver);
    } catch (e) {
      console.error(`${SCRIPT_NAME}:staffMapList 解析失败 - ${e}`);
      return null;
    }
    if (!Array.isArray(parsedData)) {
      console.error(`${SCRIPT_NAME}:staffMapList 类型错误 - 非数类型`);
      return null;
    }
    return parsedData;
  }

  // 解析 JSON 字符串中的正则表达式
  function regexReviver(key, value) {
    if (typeof value === 'string' && value.startsWith('/')) {
      const regexParttern = /^\/(.+)\/([gimsuy]*)$/;
      const match = value.match(regexParttern);
      if (match) {
        try {
          return new RegExp(match[1], match[2]);
        } catch (e) {
          throw new Error(`正则表达式 "${value}" 非法 - ${e}`);
        }
      } else throw new Error(`正则表达式 "${value}" 不符合 ${regexParttern} 格式`);
    }
    return value;
  }

  // 将正则表达式转化为字符串,以满足 JSON 格式
  function regexReplacer(key, value) {
    if (value instanceof RegExp) {
      return value.toString();
    }
    return value;
  }

  function createElement(tagName, options, subElements, eventHandlers) {
    const element = document.createElement(tagName);
    if (options) {
      for (let opt in options) {
        if (opt === 'dataset' || opt === 'style') {
          for (let key in options[opt]) {
            element[opt][key] = options[opt][key];
          }
        } else if (opt === 'class') {
          element.className = options[opt];
        } else {
          element[opt] = options[opt];
        }
      }
    }
    if (subElements) {
      updateSubElements(element, subElements);
    }
    if (eventHandlers) {
      for (let e in eventHandlers) {
        element.addEventListener(e, eventHandlers[e]);
      }
    }
    return element;
  }

  function updateSubElements(parent, subElements, isReplace = false) {
    if (isReplace) parent.innerHTML = '';
    if (!subElements) return parent;
    if (typeof subElements === 'string') subElements = [subElements];
    for (let e of subElements) {
      parent.appendChild(typeof e === 'string' ? document.createTextNode(e) : e);
    }
    return parent;
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址