Procházet zdrojové kódy

修改曲谱渲染和显示

lex před 10 měsíci
rodič
revize
0ce32f2b08

+ 3 - 3
public/osmd/index.html

@@ -19,9 +19,9 @@
       overflow: hidden;
     }
 
-    .vf-text {
+    /* .vf-text {
       display: none;
-    }
+    } */
 
     #cursorImg-0 {
       display: none;
@@ -156,4 +156,4 @@
   </script>
 </body>
 
-</html>
+</html>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/osmd/opensheetmusicdisplay.min.js


+ 6 - 0
src/tenant/music/music-detail/StringUtil.ts

@@ -0,0 +1,6 @@
+export class StringUtil {
+  public static StringContainsSeparatedWord(str: string, wordRegExString: string, ignoreCase = false): boolean {
+    const regExp = new RegExp("( |^)" + wordRegExString + "([ .]|$)", ignoreCase ? "i" : undefined);
+    return regExp.test(str);
+  }
+}

+ 2 - 2
src/tenant/music/music-detail/formatSvgToImg.ts

@@ -36,7 +36,7 @@ const blobToBase64 = (blob: any) => {
 }
 let canvas = null as any
 export const svgtopng = async (svg: any, width: any, height: any) => {
-  console.log(canvas, +new Date() + '-----')
+  // console.log(canvas, +new Date() + '-----')
 
   if (!canvas) {
     canvas = new OffscreenCanvas(width, height)
@@ -62,7 +62,7 @@ export const svgtopng = async (svg: any, width: any, height: any) => {
   // releaseCanvas(canvas)
   ctx.clearRect(0, 0, canvas.width, canvas.height)
   // canvas = null
-  console.log(canvas, 'draw')
+  // console.log(canvas, 'draw')
   await v.stop()
   v = null
   return base64

+ 352 - 0
src/tenant/music/music-detail/instrument.ts

@@ -0,0 +1,352 @@
+import { StringUtil } from "./StringUtil";
+import { isSpecialMark, isSpeedKeyword, isGradientWords, GRADIENT_SPEED_RESET_TAG } from "./speed-tag";
+
+export const formatXML = (xml: string): string => {
+  if (!xml) return "";
+  const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+  const measures: any = xmlParse.getElementsByTagName("measure");
+  // const repeats: any = Array.from(xmlParse.querySelectorAll('repeat'))
+  // 处理重复小节信息
+  // let speed = -1
+  let beats = -1;
+  let beatType = -1;
+  // 小节中如果没有节点默认为休止符
+  for (const measure of measures) {
+    if (beats === -1 && measure.getElementsByTagName("beats").length) {
+      beats = parseInt(measure.getElementsByTagName("beats")[0].textContent || "4");
+    }
+    if (beatType === -1 && measure.getElementsByTagName("beat-type").length) {
+      beatType = parseInt(measure.getElementsByTagName("beat-type")[0].textContent || "4");
+    }
+    // if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
+    //   speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed)
+    // }
+    const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
+    if (measure.getElementsByTagName("note").length === 0) {
+      const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0];
+      if (forwardTimeElement) {
+        forwardTimeElement.textContent = "0";
+      }
+      measure.innerHTML =
+        measure.innerHTML +
+        `
+        <note>
+          <rest measure="yes"/>
+          <duration>${divisions * beats}</duration>
+          <voice>1</voice>
+          <type>whole</type>
+        </note>`;
+    }
+  }
+  return new XMLSerializer().serializeToString(xmlParse);
+};
+
+
+
+export const onlyVisible = (xml: string, partIndex: number): string => {
+  if (!xml) return "";
+  const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+  const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || [];
+  const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0].textContent || "");
+  const parts: any = xmlParse.getElementsByTagName("part");
+  // const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
+  const part: any = parts[0]
+  const firstMeasures = [...part.getElementsByTagName("measure")];
+  const metronomes = [...part.getElementsByTagName("metronome")];
+  const words = [...part.getElementsByTagName("words")];
+  const codas = [...part.getElementsByTagName("coda")];
+  const rehearsals = [...part.getElementsByTagName("rehearsal")];
+
+  /** 第一分谱如果是约定的配置分谱则跳过 */
+  if (partListNames[0]?.toLocaleUpperCase?.() === "COMMON") {
+    partIndex++;
+    partListNames.shift();
+  }
+  const visiblePartInfo = partList[partIndex];
+  // console.log(visiblePartInfo, partIndex)
+  if (visiblePartInfo) {
+    const id = visiblePartInfo.getAttribute("id");
+    Array.from(parts).forEach((part: any) => {
+      if (part && part.getAttribute("id") !== id) {
+        part.parentNode?.removeChild(part);
+        // 不等于第一行才添加避免重复添加
+      } else if (part && part.getAttribute("id") !== "P1") {
+        // 速度标记仅保留最后一个
+        const metronomeData: {
+          [key in string]: Element;
+        } = {};
+        for (let i = 0; i < metronomes.length; i++) {
+          const metronome = metronomes[i];
+          const metronomeContainer = metronome.parentElement?.parentElement?.parentElement;
+          if (metronomeContainer) {
+            const index = firstMeasures.indexOf(metronomeContainer);
+            metronomeData[index] = metronome;
+          }
+        }
+        Object.values(metronomeData).forEach((metronome) => {
+          const metronomeContainer: any = metronome.parentElement?.parentElement;
+          const parentMeasure: any = metronomeContainer?.parentElement;
+          const measureMetronomes = [...(parentMeasure?.childNodes || [])];
+          const metronomesIndex = metronomeContainer ? measureMetronomes.indexOf(metronomeContainer) : -1;
+          // console.log(parentMeasure)
+          if (parentMeasure && metronomesIndex > -1) {
+            const index = firstMeasures.indexOf(parentMeasure);
+            const activeMeasure = part.getElementsByTagName("measure")[index];
+            setElementNoteBefore(metronomeContainer, parentMeasure, activeMeasure);
+            //   console.log(measureMetronomes, metronomesIndex, activeMeasure?.childNodes, activeMeasure?.childNodes[metronomesIndex])
+            //   activeMeasure?.insertBefore(metronomeContainer.cloneNode(true), activeMeasure?.childNodes[metronomesIndex])
+            //   // part.getElementsByTagName('measure')[index]?.appendChild(metronomeContainer.cloneNode(true))
+            //   // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure))
+          }
+        });
+        /** word比较特殊需要精确到note位置 */
+        // words.forEach((word) => {
+        //   let text = word.textContent || "";
+        //   text = ["cresc."].includes(text) ? "" : text;
+        //   if ((isSpecialMark(text) || isSpeedKeyword(text) || isGradientWords(text) || isRepeatWord(text) || GRADIENT_SPEED_RESET_TAG) && text) {
+        //     const wordContainer = word.parentElement?.parentElement;
+        //     const parentMeasure = wordContainer?.parentElement;
+        //     const measureWords = [...(parentMeasure?.childNodes || [])];
+        //     const wordIndex = wordContainer ? measureWords.indexOf(wordContainer) : -1;
+        //     if (wordContainer && parentMeasure && wordIndex > -1) {
+        //       const index = firstMeasures.indexOf(parentMeasure);
+        //       const activeMeasure = part.getElementsByTagName("measure")[index];
+        //       // 找当前小节是否包含word标签
+        //       const _words: any = Array.from(activeMeasure?.getElementsByTagName("words") || []);
+        //       // 遍历word标签,检查是否和第一小节重复,如果有重复则不平移word
+        //       const total = _words.reduce((total: any, _word) => {
+        //         if (_word.textContent?.includes(text)) {
+        //           total++;
+        //         }
+        //         return total;
+        //       }, 0);
+        //       if (total === 0) {
+        //         setElementNoteBefore(wordContainer, parentMeasure, activeMeasure);
+
+        //       }
+        //     }
+        //   }
+        // });
+        /** word比较特殊需要精确到note位置 */
+        // codas.forEach((coda) => {
+        //   const wordContainer = coda.parentElement?.parentElement;
+        //   const parentMeasure = wordContainer?.parentElement;
+        //   const measureWords = [...(parentMeasure?.childNodes || [])];
+        //   const wordIndex = wordContainer ? measureWords.indexOf(wordContainer) : -1;
+        //   if (wordContainer && parentMeasure && wordIndex > -1) {
+        //     const index = firstMeasures.indexOf(parentMeasure);
+        //     const activeMeasure = part.getElementsByTagName("measure")[index];
+
+        //     setElementNoteBefore(wordContainer, parentMeasure, activeMeasure);
+
+        //   }
+        // });
+        // rehearsals.forEach((rehearsal) => {
+        //   const container = rehearsal.parentElement?.parentElement;
+        //   const parentMeasure = container?.parentElement;
+        //   // console.log(rehearsal)
+        //   if (parentMeasure) {
+        //     const index = firstMeasures.indexOf(parentMeasure);
+        //     part.getElementsByTagName("measure")[index]?.appendChild(container.cloneNode(true));
+        //     // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure))
+        //   }
+        // });
+      } else {
+        // words.forEach((word) => {
+        //   const text = word.textContent || "";
+        //   if (isSpeedKeyword(text) && text) {
+        //     const wordContainer = word.parentElement?.parentElement?.parentElement;
+        //     if (wordContainer && wordContainer.firstElementChild && wordContainer.firstElementChild !== word) {
+        //       const wordParent = word.parentElement?.parentElement;
+        //       const fisrt = wordContainer.firstElementChild;
+        //       wordContainer.insertBefore(wordParent, fisrt);
+        //     }
+        //   }
+        // });
+      }
+
+      // 最后一个小节的结束线元素不在最后 调整
+      if (part && part.getAttribute("id") === id) {
+        const barlines = part.getElementsByTagName("barline");
+        const lastParent = barlines[barlines.length - 1]?.parentElement;
+        if (lastParent?.lastElementChild?.tagName !== "barline") {
+          const children: any = lastParent?.children || [];
+          for (const el of children) {
+            if (el.tagName === "barline") {
+              // 将结束线元素放到最后
+              lastParent?.appendChild(el);
+              break;
+            }
+          }
+        }
+      }
+    });
+    Array.from(partList).forEach((part) => {
+      if (part && part.getAttribute("id") !== id) {
+        part.parentNode?.removeChild(part);
+      }
+    });
+    // 处理装饰音问题
+    const notes = xmlParse.getElementsByTagName("note");
+    const getNextvNoteDuration = (i: number) => {
+      let nextNote = notes[i + 1];
+      // 可能存在多个装饰音问题,取下一个非装饰音时值
+      for (let index = i; index < notes.length; index++) {
+        const note = notes[index];
+        if (!note.getElementsByTagName("grace")?.length) {
+          nextNote = note;
+          break;
+        }
+      }
+      const nextNoteDuration = nextNote?.getElementsByTagName("duration")[0];
+      return nextNoteDuration;
+    };
+    Array.from(notes).forEach((note, i) => {
+      const graces = note.getElementsByTagName("grace");
+      if (graces && graces.length) {
+        // if (i !== 0) {
+        // note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
+        // }
+      }
+    });
+  }
+  // console.log(xmlParse)
+  return new XMLSerializer().serializeToString(appoggianceFormate(xmlParse));
+};
+
+
+// 倚音后连音线
+export const appoggianceFormate = (xmlParse: Document): Document => {
+  if (!xmlParse) return xmlParse;
+  const graces: any = xmlParse.querySelectorAll("grace");
+  if (!graces.length) return xmlParse;
+  const getNextElement = (el: HTMLElement): HTMLElement => {
+    if (el.querySelector("grace")) {
+      return getNextElement(el?.nextElementSibling as HTMLElement);
+    }
+    return el;
+  };
+  for (const grace of graces) {
+    const notations = grace.parentElement?.querySelector("notations");
+    if (notations && notations.querySelectorAll("slur").length > 1) {
+      const nextEle: Element = getNextElement(grace.parentElement?.nextElementSibling as HTMLElement);
+      if (nextEle && nextEle.querySelectorAll("slur").length > 0) {
+        const slurNumber = Array.from(nextEle.querySelector("notations")?.children || []).map((el: Element) => {
+          return el.getAttribute("number");
+        });
+        const slurs = notations.querySelectorAll("slur");
+        for (const nota of slurs) {
+          if (!slurNumber.includes(nota.getAttribute("number"))) {
+            nextEle.querySelector("notations")?.appendChild(nota);
+          }
+        }
+      }
+    }
+  }
+  return xmlParse;
+};
+
+
+/**
+ * 添加第一分谱信息至当前分谱
+ * @param ele 需要插入的元素
+ * @param fitstParent 合奏谱第一个分谱
+ * @param parent 需要添加的分谱
+ */
+const setElementNoteBefore = (ele: Element, fitstParent: Element, parent?: Element | null) => {
+  let noteIndex = 0;
+  if (!fitstParent) {
+    return;
+  }
+  for (let index = 0; index < fitstParent.childNodes.length; index++) {
+    const element = fitstParent.childNodes[index];
+    if (element.nodeName === "note") {
+      noteIndex++;
+    }
+    if (element === ele) {
+      break;
+    }
+  }
+  if (noteIndex === 0 && parent) {
+    parent.insertBefore(ele, parent.childNodes[0]);
+    return;
+  }
+  if (parent && parent.childNodes.length > 0) {
+    let noteIndex2 = 0;
+    const notes = Array.from(parent.childNodes).filter((child) => child.nodeName === "note");
+    const lastNote = notes[notes.length - 1];
+    if (noteIndex >= notes.length && lastNote) {
+      parent.insertBefore(ele, parent.childNodes[Array.from(parent.childNodes).indexOf(lastNote)]);
+      return;
+    }
+    for (let index = 0; index < notes.length; index++) {
+      const element = notes[index];
+      if (element.nodeName === "note") {
+        noteIndex2 = noteIndex2 + 1;
+        if (noteIndex2 === noteIndex) {
+          parent.insertBefore(ele, element);
+          break;
+        }
+      }
+    }
+  }
+  // console.log(noteIndex, parent)
+};
+
+
+
+
+/**
+ * 检查传入文字是否为重复关键词
+ * @param text 总谱xml
+ * @returns 是否是重复关键词
+ */
+export const isRepeatWord = (text: string): boolean => {
+  if (text) {
+    const innerText = text.toLocaleLowerCase();
+    const dsRegEx = "d\\s?\\.s\\.";
+    const dcRegEx = "d\\.\\s?c\\.";
+
+    return (
+      innerText === "@" ||
+      StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al fine", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al fine", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "da\\s?capo", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "dal\\s?segno", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "al\\s?coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "to\\s?coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "a (la )?coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "fine", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "coda", true) ||
+      StringUtil.StringContainsSeparatedWord(innerText, "segno", true)
+    );
+  }
+  return false;
+};
+
+
+
+/** 从xml中获取自定义信息,并删除多余的字符串 */
+export const getCustomInfo = (xml: string): any => {
+  const data = {
+    showSpeed: true,
+    parsedXML: xml,
+  };
+  const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+  const words: any = xmlParse.getElementsByTagName("words");
+  for (const word of words) {
+    if (word && word.textContent?.trim() === "隐藏速度") {
+      data.showSpeed = false;
+      word.textContent = "";
+    }
+    if (word && word.textContent?.trim() === "@") {
+      word.textContent = "segno";
+    }
+  }
+  data.parsedXML = new XMLSerializer().serializeToString(xmlParse);
+  return data;
+};

+ 38 - 34
src/tenant/music/music-detail/new-index.tsx

@@ -63,6 +63,7 @@ import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload'
 import { svgtopng } from './formatSvgToImg'
 import { shareCall } from '@/teacher/share-page/share'
 import deepClone from '@/helpers/deep-clone'
+import { formatXML, getCustomInfo, onlyVisible } from './instrument'
 
 export default defineComponent({
   name: 'new-index',
@@ -248,16 +249,6 @@ export default defineComponent({
           ...dataObj,
           file: files
         })
-        // for (const key in dataObj) {
-        //   formData.append(key, dataObj[key])
-        // }
-
-        // formData.append('file', files, fileName)
-        // const ossUploadUrl = getOssUploadUrl('cloud-coach')
-        // await umiRequest(ossUploadUrl, {
-        //   method: 'POST',
-        //   data: formData
-        // })
         Toast.clear()
         // const imgurl = getOssUploadUrl('cloud-coach') + keyTime
 
@@ -712,25 +703,10 @@ export default defineComponent({
 
         staffData.partList = tempPartList
 
-        // staffData.partList.forEach((part: any) => {
-        //   const item = staffData.xmlPartList.find(
-        //     item => item.name === part.track
-        //   )
-        //   if (item) {
-        //     part.index = item.value
-        //   }
-        // })
-        // console.log(staffData.partList, '-staffData.partList')
         staffData.tempPartList = JSON.parse(JSON.stringify(staffData.partList))
         staffData.partList = instrumentSort(staffData.partList)
         staffData.partXmlIndex = staffData.partList[0].index || 0
 
-        // console.log(
-        //   staffData.partList,
-        //   'parts',
-        //   staffData.xmlPartList,
-        //   musicDetail.value?.background
-        // )
         staffData.instrumentName = getInstrumentName(
           staffData.partList[staffData.partIndex]?.track
         )
@@ -739,30 +715,58 @@ export default defineComponent({
       }
     }
 
-    const musicIframeLoad = () => {
+    const musicIframeLoad = async () => {
       const iframeRef: any = document.getElementById('staffIframeRef')
       if (iframeRef && iframeRef.contentWindow.renderXml) {
-        iframeRef.contentWindow.renderXml(
-          staffData.musicXml,
-          staffData.partXmlIndex
-        )
+        const res = await umiRequest.get(staffData.musicXml, {
+          mode: 'cors'
+        })
+        const parseXmlInfo = getCustomInfo(res)
+        const xml = formatXML(parseXmlInfo.parsedXML)
+
+        const currentXml = onlyVisible(xml, staffData.partXmlIndex)
+        iframeRef.contentWindow.renderXml(currentXml, staffData.partXmlIndex)
+
+        // iframeRef.contentWindow.renderXml(
+        //   staffData.musicXml,
+        //   staffData.partXmlIndex
+        // )
       }
     }
-    const resetRender = () => {
+    const resetRender = async () => {
       const iframeRef: any = document.getElementById('staffIframeRef')
       if (iframeRef && iframeRef.contentWindow.renderXml) {
-        iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
+        loading.value = true
+        // iframeRef.contentWindow.resetRender(staffData.partXmlIndex)
+        const res = await umiRequest.get(staffData.musicXml, {
+          mode: 'cors'
+        })
+        const parseXmlInfo = getCustomInfo(res)
+        const xml = formatXML(parseXmlInfo.parsedXML)
+
+        const currentXml = onlyVisible(xml, staffData.partXmlIndex)
+        iframeRef.contentWindow.renderXml(currentXml, 0)
+
         staffData.instrumentName = getInstrumentName(
           staffData.partList[staffData.partIndex]?.track
         )
       }
     }
 
-    const resetRenderPage = (type: string, xmlUrl: string) => {
+    const resetRenderPage = async (type: string, xmlUrl: string) => {
       const iframeRef: any = document.getElementById('staffIframeRef')
       if (iframeRef && iframeRef.contentWindow.renderXml) {
         // console.log('resetRenderPage')
-        iframeRef.contentWindow.resetRenderPage(type, xmlUrl)
+        // iframeRef.contentWindow.resetRenderPage(type, xmlUrl)
+
+        const res = await umiRequest.get(staffData.musicXml, {
+          mode: 'cors'
+        })
+        const parseXmlInfo = getCustomInfo(res)
+        const xml = formatXML(parseXmlInfo.parsedXML)
+
+        const currentXml = onlyVisible(xml, staffData.partXmlIndex)
+        iframeRef.contentWindow.resetRenderPage(type, currentXml)
       }
     }
     const partColumns = computed(() => {

+ 88 - 0
src/tenant/music/music-detail/speed-tag.ts

@@ -0,0 +1,88 @@
+/**
+  Grava壮板=40
+  Largo广板=46
+  Lento慢板=52
+  Adagio柔板=56
+  Larghetto小广板=60
+  Andante行板=66
+  Anderato/Andantino小行板=69
+  Moderato中板=88
+  Allegretto小快板=108
+  Allegro Moderato=108 // 考级需要
+  Allegro快板=132
+  Vivace快速有生气=152
+  Vivo快速有生气=160
+  Vivacissimo极其活泼的快板=168
+  Presto急板=184
+  Prestissimo最急板=208
+ */
+
+// import {TextAlignmentEnum} from "../../Common/Enums/TextAlignment";
+
+export const SpeedTag: { [key in string]: number } = {
+  "Grava": 40,
+  "Largo": 46,
+  "Lento": 52,
+  "Adagio": 56,
+  "Larghetto": 60,
+  "Andante": 66,
+  "Anderato": 69,
+  "Andantino": 69,
+  "Moderato": 88,
+  "Allegretto": 108,
+  "Allegro Moderato": 108,
+  "Allegro": 132,
+  "Vivace": 152,
+  "Vivo": 160,
+  "Vivacissimo": 168,
+  "Presto": 184,
+  "Prestissimo": 208
+};
+
+export const SpecialMarks: string[] = ["纯律", "纯律结束"];
+
+export const HideWords: string[] = ["跳过下一个", "b", "#", "§", "º", "X"];
+
+export const GradientWords: string[] = ["poco rit.", "rall.", "rit.", "accel.", "molto rit.", "molto rall", "lentando", "poco accel.", "calando"];
+
+export const GRADIENT_SPEED_CLOSE_TAG = "结束范围速度";
+
+export const GRADIENT_SPEED_RESET_TAG = "a tempo";
+
+export const SpecialWords: string[] = [GRADIENT_SPEED_CLOSE_TAG];
+
+export const SpeedKeyword = "速度 ";
+export const SpeedHiddenKeyword = "仅文本速度 ";
+
+/** 是否为速度关键词 */
+export function isSpeedKeyword(str: string): boolean {
+  return str.indexOf(SpeedKeyword) === 0;
+};
+
+/** 是否为速度关键词 */
+export function isSpeedHiddenKeyword(str: string): boolean {
+  return str.indexOf(SpeedHiddenKeyword) === 0;
+};
+
+/** 格式化速度关键词移除前缀 */
+export function formatSpeedKeyword(str: string): string {
+  return str.replace(SpeedHiddenKeyword, "").replace(SpeedKeyword, "");
+};
+
+/** 是否是渐变速度关键词 */
+export function isGradientWords(str: string): boolean {
+  return GradientWords.includes(str);
+};
+
+/**
+ * 是否为特殊标记
+ * 包含中文与英文,中文正则存在部分问题
+ */
+export function isSpecialMark(str: string): boolean {
+  return [...Object.keys(SpeedTag), ...SpecialMarks, ...SpecialWords, ...HideWords]
+    .map((s: string) => s.trim().toLocaleUpperCase()).includes(str.toLocaleUpperCase().trim());
+}
+
+// export function isTopFont(textAlignment): boolean {
+//   return [TextAlignmentEnum.CenterTop, TextAlignmentEnum.RightTop].includes(textAlignment);
+// };

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů