教程:在Typora中显示拼音——附处理工具

news2025/4/13 8:15:21

原因

因为自己普通话不标准,希望可以制作适合自己的带拼音的文档,可以把平常看到的内容、说过的话作为练习普通话的材料。

在市面上,带拼音的材料、书籍并不多,而且有可能是一些比较生僻的内容。所以希望可以自己制作这样的材料。之前也尝试过:【word + word百宝箱插件 】的方式来做材料。但个人原因,不是很喜欢用 word 文档的方式,更喜欢 Markdown 的方式。

平常自己用的最多的就是 Typora,但没有找到现成的处理工具,所以,就自己开发了一个。

这个工具就是一个 html 文件,所以,复制我提供的完整代码,保存到 html 文件中,双击使用浏览器打开即可使用全部功能。

效果图

处理前的文本排版样式:

在这里插入图片描述

处理后的样式:

在这里插入图片描述

可以选中后批量突出显示:

在这里插入图片描述

提供现成的 CSS 样式,且可以一键复制:
在这里插入图片描述

提供自动渲染脚本:

在这里插入图片描述

提供导出 PDF 时需要的 CSS 样式代码,一键复制过去即可使用,还提供了两个在线转换的工具,不需要记忆网址,直接点击即可在新的标签页中打开:

在这里插入图片描述

堆叠式通知:

在这里插入图片描述

在 Typora 中渲染出来的效果【效果与主题字体有关】:

在这里插入图片描述

在 PDF 中的效果:

在这里插入图片描述

这就是测试过程中用的 Typora 的默认 github 主题的拷贝,所以,字体比较丑,重要的是,可以突出显示拼音和汉字。

原理

除了 html 外,我就只知道 word 可以做到 汉字顶部带拼音的效果。而 word 可以做到,也是因为它是 xml 结构的。

Tip:word 文档的后缀 .docx ,其中 doc 就是 document 的意思,而其中的字母 x ,其实就是 xml 的意思。你可以随便创建一个后缀为 .docx 的 word 文档,然后修改后缀为 zip ,然后用解压软件解压,就可以看到许多 xml 文件。如果这里面有带拼音的内容,就可以看出,它的结构与html中的ruby标签类似。

而在 Typora 中查看 Markdown 文件,它就是被渲染成 html 的,所以,可以将汉字与拼音用 ruby 标签来包裹,达到我们想要的上下结构的显示效果。

如果人为手动的修改,那工作量会大得吓人。所以才写了这个工具来批量将普通的文本转为用ruby标签包裹的文本

使用流程

先复制我提供的代码到 html 文件中,然后用浏览器打开。你就可以再网页的底部找到两个链接,鼠标左键单击就可以在新的标签页中打开。


然后,复制你需要添加拼音的文本到这两个网站中的其中一个即可,根据网页上的按钮和提示,自己操作。

注意:需要自己检查多音字,两个网站都可以切换多音字的读音。先把拼音修改正确后,再复制到这个小工具中,进行转换。


当把转换后的内容复制到 Typora 中后,它是不会像这个工具的 【Ruby标签结果】窗口那样,渲染出你突出显示的内容的。也就是不会显示为红色加粗的样式。

所以,接下来就是复制这个工具中提供的 CSS 样式,到你现在 Typora 正在使用的主题中。保存重启 Typora ,查看之前粘贴的 带拼音的内容。


这个时候,你会发现,依然没有效果。。。。

其实这是 Typora 这个软件本身的问题,它渲染后的 html 结构太乱了,你自己在开发者工具中查看就知道我为什么说它乱了。。。

正由于它的结构乱,所以,CSS选择器无法选择想要修改的内容,这个时候就需要 JS 代码来操作。


此时,就需要复制这个网页工具上的 :渲染脚本,到 window.html 文件中。

修改 window.html 的方法,我之前的一篇文章中,有写到过:【非主题的方式】Typora 标题自动编号功能的实现——全网首发!-CSDN博客

操作完后,重启 Typora ,就可以看到那些你突出显示的字和拼音,被用红色加粗的样式 凸显出来了。样式,可以自己在代码中修改。

这段脚本是自动渲染的,也提供了一个快捷键:F7 来一键处理并渲染样式。

顺便提一下,如果你不喜欢这种自动渲染的方式,也可以复制这段代码,让AI来修改,让它:去掉其中的观察代码,改为使用快捷键调用的方式,并让它保留通知的效果,即可。


其实,如果只是做成,汉字黑色,拼音统一红色的上下结构。一点都不难。

但要突出那些需要重点记忆的字或者词,真的麻烦。可以说,这个工具 接近 1000 行代码中,有一半以上的代码都是在做这个功能。


如果你还想将这个文档导出为 PDF 格式的话,那么就还需要接着做。

复制这个网页工具中,关于 PDF 的那部分代码到 你 Typora 正在使用的主题中。重启后,再导出,才可以让 需要突出显示的 字 或 词的样式被保留在 PDF 文件中。

王婆卖瓜自卖自夸

这个网页工具主要特点如下:

  1. 支持两种输入格式:支持 【“中zhōng”】 和 【“中(zhōng)”】 两种常见的拼音标注格式
  2. 即时转换:一键转换,结果立刻可见
  3. 本地使用:数据安全,不需要网络,无需等待
  4. 高亮标记重点词:可以通过右键点击让特定汉字及其拼音以红色显示,方便标记重点词汇
  5. 批量高亮:可以选中多个汉字,右键点击一次性批量高亮
  6. 一键复制:复制结果后可直接粘贴到支持HTML的编辑器中使用
  7. 适配Typora:提供了专用CSS样式、自动渲染脚本,完美适配Typora编辑器,及其导出时需要的 PDF 的样式
  8. 友好的提示系统:带进度条的堆叠式通知,操作反馈清晰明了
  9. 美观的界面:简洁大方的设计,操作体验一流

。。。。

扩展:增加一键给标题编号的功能

我平常用 Typora 写 Markdown 的时候,会需要它自动排序的功能。

很明显这个功能,Typora 也没有。所以,也就自己写了一个。

方法参考之前提到的那篇文章:【非主题的方式】Typora 标题自动编号功能的实现——全网首发!-CSDN博客

如果你喜欢用命令行工具来排序 Markdown 的标题,那么可以试试这个工具,只有 17KB 的大小:Markdown标题序号处理工具——用 C 语言实现-CSDN博客

如果你喜欢本地网页工具的方式来处理标题编号的问题,那么可以看看:用 HTML 网页来管理 Markdown 标题序号-CSDN博客

完整源代码

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>拼音排版转Ruby标签工具</title>
  <style>
    :root {
      --primary-color: #3498db;
      --primary-hover: #2980b9;
      --success-color: #2ecc71;
      --success-hover: #27ae60;
      --danger-color: #e74c3c;
      --danger-hover: #c0392b;
      --text-color: #333;
      --light-gray: #f5f5f5;
      --border-color: #ddd;
      --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }

    body {
      font-family: "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background-color: var(--light-gray);
      color: var(--text-color);
      line-height: 1.6;
      position: relative;
      min-height: 100vh;
      padding-bottom: 120px;
      /* 为底部链接预留更多空间 */
    }

    h1,
    h2,
    h3 {
      color: #2c3e50;
      margin-top: 0;
    }

    h1 {
      text-align: center;
      margin-bottom: 30px;
      font-size: 2.2em;
      font-weight: 600;
      background: linear-gradient(135deg, #3498db, #2c3e50);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--border-color);
    }

    .container {
      display: flex;
      flex-direction: column;
      gap: 25px;
    }

    .card {
      background-color: white;
      border-radius: 8px;
      padding: 20px;
      box-shadow: var(--shadow);
    }

    textarea {
      width: 96%;
      height: 180px;
      padding: 15px;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 16px;
      line-height: 1.6;
      resize: none;
      transition: border-color 0.3s, box-shadow 0.3s;
    }

    textarea:focus {
      outline: none;
      border-color: var(--primary-color);
      box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
    }

    .btns {
      display: flex;
      justify-content: space-between;
      gap: 10px;
      margin-top: 15px;
    }

    button {
      flex: 1;
      background-color: var(--primary-color);
      color: white;
      border: none;
      padding: 12px 20px;
      font-size: 16px;
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.3s;
      font-weight: 500;
    }

    button:hover {
      background-color: var(--primary-hover);
      transform: translateY(-1px);
    }

    button:active {
      transform: translateY(0);
    }

    .btn-clear {
      background-color: #95a5a6;
    }

    .btn-clear:hover {
      background-color: #7f8c8d;
    }

    .output-container {
      position: relative;
    }

    .output {
      width: 94%;
      min-height: 180px;
      padding: 20px;
      background-color: white;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 18px;
      line-height: 2.2;
      white-space: pre-wrap;
      word-wrap: break-word;
      overflow-y: auto;
      max-height: 300px;
    }

    .copy-btn {
      position: absolute;
      top: 10px;
      right: 10px;
      background-color: var(--success-color);
      padding: 8px 15px;
      font-size: 14px;
      border-radius: 4px;
    }

    .copy-btn:hover {
      background-color: var(--success-hover);
    }

    /* 提取公共样式,创建一个新的类 */
    .typora-section {
      margin-top: 30px;
      margin-bottom: 30px;
    }

    .typora-section h3 {
      margin-bottom: 15px;
    }

    .typora-section p {
      margin-bottom: 15px;
      color: #555;
    }

    /* 将特定的差异样式单独应用 */
    .typora-pdf-code {
      margin-bottom: 60px;
    }

    pre {
      background-color: #f8f9fa;
      padding: 15px;
      border-radius: 6px;
      overflow: auto;
      position: relative;
      line-height: 1.5;
      max-height: 200px;
      border: 1px solid var(--border-color);
    }

    #copyThemeBtn,
    #copyPDFBtn,
    #copyRenderScript {
      margin-bottom: 15px;
      background-color: #9b59b6;
    }

    #copyThemeBtn:hover,
    #copyPDFBtn:hover,
    #copyRenderScript:hover {
      background-color: #8e44ad;
    }

    /* Ruby标签样式 */
    ruby {
      ruby-position: over;
      margin-right: 0.5em;
    }

    rt {
      font-size: 0.65em;
      color: var(--text-color);
      text-align: center;
      padding-bottom: 3px;
      letter-spacing: normal;
      font-weight: 500;
    }

    /* 高亮样式 */
    ruby.highlighted rb,
    ruby.highlighted rt,
    .highlighted rb,
    .highlighted rt,
    .highlighted {
      color: var(--danger-color) !important;
      font-weight: bold !important;
    }

    rb {
      letter-spacing: 0.5em;
      position: relative;
      left: 0.25em;
    }

    /* 提示信息样式 */
    .toast-container {
      position: fixed;
      top: 20px;
      right: 20px;
      display: flex;
      flex-direction: column;
      gap: 10px;
      z-index: 1000;
      max-height: 90vh;
      overflow-y: auto;
      padding-right: 5px;
      /* 避免滚动条贴边 */
    }

    .toast {
      background-color: #2ecc71;
      color: white;
      padding: 12px 20px 20px;
      /* 增加底部padding为进度条留出空间 */
      border-radius: 4px;
      box-shadow: var(--shadow);
      opacity: 0;
      transition: opacity 0.3s, transform 0.3s;
      transform: translateY(-10px);
      position: relative;
      min-width: 200px;
    }

    .toast.show {
      opacity: 1;
      transform: translateY(0);
    }

    .toast.error {
      background-color: var(--danger-color);
    }

    .toast.info {
      background-color: var(--primary-color);
    }

    /* 进度条样式 */
    .toast-progress {
      position: absolute;
      bottom: 0;
      left: 0;
      height: 4px;
      background-color: rgba(255, 255, 255, 0.7);
      width: 100%;
      border-radius: 0 0 4px 4px;
      transform-origin: left;
      will-change: transform;
      backface-visibility: hidden;
    }

    /* 右键菜单 */
    .context-menu {
      display: none;
      position: fixed;
      background-color: rgb(67, 236, 138);
      border-radius: 4px;
      box-shadow: var(--shadow);
      z-index: 100;
      min-width: 150px;
    }

    .context-menu-item {
      padding: 10px 15px;
      cursor: pointer;
      transition: background-color 0.2s;
    }

    /* .context-menu-item:hover {
            background-color: var(--light-gray);
        } */

    /* 返回顶部按钮 */
    .back-to-top {
      position: fixed;
      bottom: 30px;
      right: 30px;
      width: 50px;
      height: 50px;
      background-color: var(--primary-color);
      color: white;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      transition: all 0.3s;
      box-shadow: var(--shadow);
      opacity: 0;
      visibility: hidden;
      z-index: 999;
    }

    .back-to-top.show {
      opacity: 1;
      visibility: visible;
    }

    .back-to-top:hover {
      background-color: var(--primary-hover);
      transform: translateY(-3px);
    }

    /* 底部链接区域 */
    .footer-links {
      margin-top: 40px;
      padding: 20px;
      background-color: white;
      border-radius: 8px;
      box-shadow: var(--shadow);
      text-align: center;
      position: absolute;
      bottom: 20px;
      left: 20px;
      right: 20px;
    }

    .footer-links h3 {
      margin-top: 0;
      margin-bottom: 15px;
      color: #2c3e50;
    }

    .link-container {
      display: flex;
      justify-content: center;
      gap: 20px;
      flex-wrap: wrap;
    }

    .external-link {
      display: inline-flex;
      align-items: center;
      padding: 10px 15px;
      background-color: #f8f9fa;
      border-radius: 8px;
      color: var(--primary-color);
      text-decoration: none;
      transition: all 0.3s;
      font-weight: 500;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
    }

    .external-link:hover {
      background-color: var(--primary-color);
      color: white;
      transform: translateY(-2px);
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    }

    .external-link svg {
      margin-right: 8px;
    }
  </style>
</head>

<body>
  <h1>拼音排版转Ruby标签工具</h1>

  <div class="container">
    <div class="card">
      <h3>输入文本:</h3>
      <textarea id="inputText" placeholder="请粘贴拼音标注文本,例如:中zhōng 文wén 汉hàn 字zì 或 中(zhōng) 文(wén) 汉(hàn) 字(zì)">中zhōng 文wén 汉hàn 字zì 转zhuǎn 拼pīn 音yīn 工gōng 具jù
将jiāng 汉hàn 字zì 转zhuǎn 换huàn 为wèi 中zhōng 文wén 拼pīn 音yīn ,支zhī 持chí 显xiǎn 示shì 声shēng 调diào ,常cháng 见jiàn 多duō 音yīn 字zì 智zhì 能néng 转zhuǎn 换huàn

多duō 音yīn 字zì 示shì 例lì :
三sān 百bǎi 六liù 十shí 行háng ,我wǒ 最zuì 行xíng</textarea>
      <div class="btns">
        <button id="clearBtn" class="btn-clear">清空</button>
        <button id="convertBtn">转换</button>
      </div>
    </div>

    <div class="card output-container">
      <h3>Ruby标签结果:</h3>
      <div id="output" class="output"></div>
      <button id="copyBtn" class="copy-btn">复制结果</button>
    </div>
  </div>

  <div class="card typora-section typora-theme-code">
    <h3>Typora 主题设置</h3>
    <p>想要在 Typora 中得到与预览窗口中一样的效果,请将以下 CSS 代码添加到你的 Typora 主题文件中:</p>
    <button id="copyThemeBtn">复制主题代码</button>
    <pre id="preThemeCode"><code>/* 添加到Typora的theme.css文件 */
ruby {
    ruby-position: over;
    margin-right: 0.5em;
    line-height: 2.2; /* 增加行高 */
}

rt {
    font-size: 0.65em;
    color: #333; /* 默认与文本颜色相同 */
    padding-bottom: 3px;
    font-weight: 500;
}

rb {
    letter-spacing: 0.5em;
    position: relative;
    left: 0.25em;
}

/* Typora中高亮样式 - 通过JS注入实现 */
.typora-highlighted ruby,
.typora-highlighted rt,
.typora-highlighted rb {
    color: #e74c3c !important;
    font-weight: bold !important;
}

/* 调整段落间距 */
p {
    line-height: 1.8;
    margin-bottom: 1em;
}</code></pre>
  </div>

  <div class="card typora-section typora-render-code">
    <h3>Typora 自动渲染脚本</h3>
    <p>想要在 Typora 中得到与预览窗口中一样的效果【个别字突出显示】,请将以下代码添加到 Typora 的 window.html 文件中:</p>
    <button id="copyRenderScript">复制渲染脚本</button>
    <!-- 想要让 Script 标签中的代码不被执行,那么就需要将 Script 标签左右两边的大于小于用特殊字符代替。 -->
    <pre id="preRenderCode"><code>&lt;script&gt;
  // 全局高亮计数器
  let totalHighlightCount = 0;

  const showNotification = (message, isSuccess = true) => {
    let toastContainer = document.getElementById('custom-toast-container');
    if (!toastContainer) {
      toastContainer = document.createElement('div');
      toastContainer.id = 'custom-toast-container';
      toastContainer.style = `
            position: fixed;
            top: 20px;
            right: 20px;
            display: flex;
            flex-direction: column;
            gap: 10px;
            z-index: 9999;
            max-height: 90vh;
            overflow-y: auto;
            padding-right: 5px;
        `;
      document.body.appendChild(toastContainer);
    }

    const toast = document.createElement('div');
    toast.style = `
        background: ${isSuccess ? '#4CAF50' : '#F44336'};
        color: white;
        padding: 12px 20px 20px;
        border-radius: 4px;
        box-shadow: 0 3px 10px rgba(0,0,0,0.2);
        position: relative;
        min-width: 200px;
        opacity: 0;
        transform: translateY(-10px);
        transition: opacity 0.3s, transform 0.3s;
    `;
    toast.textContent = message;

    const progressBar = document.createElement('div');
    progressBar.style = `
        position: absolute;
        bottom: 0;
        left: 0;
        height: 4px;
        background: rgba(255,255,255,0.7);
        width: 100%;
        border-radius: 0 0 4px 4px;
        transform-origin: left;
    `;
    toast.appendChild(progressBar);

    toastContainer.appendChild(toast);

    setTimeout(() => {
      toast.style.opacity = '1';
      toast.style.transform = 'translateY(0)';
      progressBar.style.transition = 'transform 3s linear';
      progressBar.style.transform = 'scaleX(0)';
    }, 10);

    setTimeout(() => {
      toast.style.opacity = '0';
      setTimeout(() => {
        if (toast.parentNode === toastContainer) {
          toastContainer.removeChild(toast);
        }
      }, 300);
    }, 3000);
  };

  let observer;

  function initObserver() {
    const config = {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
      attributeFilter: ['class']
    };

    observer = new MutationObserver(function(mutations) {
      const needsUpdate = mutations.some(mutation => {
        return mutation.type === 'characterData' ||
          (mutation.type === 'childList' && mutation.addedNodes.length > 0) ||
          (mutation.type === 'attributes' && mutation.attributeName === 'class');
      });

      if (needsUpdate) {
        debouncedProcessRubyHighlights();
      }
    });

    observer.observe(document.body, config);
    showNotification('拼音高亮观察器已启动', true);
  }

  let debounceTimer;
  function debouncedProcessRubyHighlights() {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      processRubyHighlights(false); // 自动触发时不显示通知
    }, 300);
  }

  function processRubyHighlights(showNotice = true) {
    const rawInlines = document.querySelectorAll('.md-raw-inline');
    let currentHighlightCount = 0;
    
    rawInlines.forEach(rawInline => {
      const content = rawInline.textContent || '';
      const rubyContainer = rawInline.previousElementSibling;

      if (!rubyContainer || !rubyContainer.classList.contains('md-ruby-container')) {
        return;
      }

      if (content.includes('class="highlighted"')) {
        rubyContainer.classList.add('typora-highlighted');
        currentHighlightCount++;
      } else {
        rubyContainer.classList.remove('typora-highlighted');
      }
    });
    
    // 更新全局高亮计数器
    totalHighlightCount = currentHighlightCount;
    
    // 只有明确要求显示通知时才显示
    if (showNotice) {
      showNotification(`已处理拼音高亮\n当前高亮: ${totalHighlightCount}处`, true);
    }
  }

  document.addEventListener('DOMContentLoaded', function() {
    initObserver();
    processRubyHighlights(false); // 初始化时不显示通知

    document.addEventListener('keydown', function(e) {
      if (e.key === 'F7' || e.keyCode === 118) {
        processRubyHighlights(true); // 手动刷新时显示通知
        e.preventDefault();
      }
    });
  });

  window.addEventListener('beforeunload', function() {
    if (observer) {
      observer.disconnect();
    }
  });

  document.addEventListener('paste', function() {
    setTimeout(() => {
      debouncedProcessRubyHighlights();
      showNotification('检测到粘贴操作,正在处理拼音高亮...', true);
    }, 100);
  });
&lt;/script&gt;</code></pre>
  </div>

  <div class="card typora-section typora-pdf-code">
    <h3>Typora 导出 PDF 样式</h3>
    <p>想要在导出的 PDF 文件中得到与预览窗口中一样的效果,请将以下 CSS 代码添加到你的 Typora 主题文件中:</p>
    <button id="copyPDFBtn">复制PDF所需样式</button>
    <pre id="copyPDFCode"><code>/* 添加到你的主题 CSS 文件末尾 */
@media print, (prefers-color-scheme: print) {
  ruby.highlighted,
  ruby.highlighted rt {
    color: #ff0000 !important;
    font-weight: bold !important;
  }
}</code></pre>
  </div>

  <!-- 底部链接 -->
  <div class="footer-links">
    <h3>相关工具网站</h3>
    <div class="link-container">
      <a href="https://www.lddgo.net/convert/pinyin" class="external-link" target="_blank">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
          <polyline points="15 3 21 3 21 9"></polyline>
          <line x1="10" y1="14" x2="21" y2="3"></line>
        </svg>
        汉字转拼音 - LDDGO
      </a>
      <a href="https://www.qqxiuzi.cn/zh/pinyin/" class="external-link" target="_blank">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
          <polyline points="15 3 21 3 21 9"></polyline>
          <line x1="10" y1="14" x2="21" y2="3"></line>
        </svg>
        汉字拼音转换 - 千千秀字
      </a>
    </div>
  </div>

  <!-- 提示信息容器 -->
  <div class="toast-container" id="toastContainer"></div>

  <!-- 右键菜单 -->
  <div class="context-menu" id="contextMenu">
    <div class="context-menu-item" id="highlightMenuItem">高亮/取消高亮</div>
  </div>

  <!-- 返回顶部按钮 -->
  <div class="back-to-top" id="backToTop">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
      stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="18 15 12 9 6 15"></polyline>
    </svg>
  </div>

  <script>
    // DOM元素
    const convertBtn = document.getElementById('convertBtn');
    const inputText = document.getElementById('inputText');
    const output = document.getElementById('output');
    const clearBtn = document.getElementById('clearBtn');
    const copyBtn = document.getElementById('copyBtn');
    const copyThemeBtn = document.getElementById('copyThemeBtn');
    const preThemeCode = document.getElementById('preThemeCode');
    const toastContainer = document.getElementById('toastContainer');
    const contextMenu = document.getElementById('contextMenu');
    const highlightMenuItem = document.getElementById('highlightMenuItem');
    const backToTop = document.getElementById('backToTop');
    const copyRenderScript = document.getElementById('copyRenderScript');
    const preRenderCode = document.getElementById('preRenderCode');
    const copyPDFBtn = document.getElementById('copyPDFBtn');
    const copyPDFCode = document.getElementById('copyPDFCode');

    // 转换按钮点击事件
    convertBtn.addEventListener('click', function () {
      if (!inputText.value.trim()) {
        showToast('内容为空,请先输入文本!', 'error');
        return;
      }

      const rubyText = convertToRuby(inputText.value);
      output.innerHTML = rubyText;
      showToast('转换完成!', 'success');

      // 添加右键菜单事件
      setupRubyHighlighting();
    });

    // 清空按钮点击事件
    clearBtn.addEventListener('click', function () {
      if (!inputText.value.trim()) {
        showToast('内容已为空!', 'info');
        return;
      }

      inputText.value = '';
      output.innerHTML = '';
      showToast('已清空!', 'info');
    });

    // 复制结果按钮点击事件
    copyBtn.addEventListener('click', async function () {
      const content = output.innerHTML.trim();

      if (!content) {
        showToast('内容为空,无法复制!', 'error');
        return;
      }

      try {
        await navigator.clipboard.writeText(content);
        showToast('结果已复制!', 'success');
      } catch (err) {
        showToast('复制失败!', 'error');
      }
    });

    // 复制主题代码按钮点击事件
    copyThemeBtn.addEventListener('click', async function () {
      try {
        await copyToClipboardWithDelay(preThemeCode.textContent);
        showToast('主题代码已复制!', 'success');
      } catch (err) {
        showToast('复制失败!', 'error');
      }
    });

    // 复制渲染脚本按钮点击事件
    copyRenderScript.addEventListener('click', async function() {
      try {
        await copyToClipboardWithDelay(preRenderCode.textContent);
        showToast('渲染脚本代码已复制!', 'success');
      } catch (err) {
        showToast('复制失败!', 'error');
      }
    });

    // 复制 PDF 样式按钮点击事件
    copyPDFBtn.addEventListener('click', async function() {
      try {
        await copyToClipboardWithDelay(copyPDFCode.textContent);
        showToast('PDF 样式代码已复制!', 'success');
      } catch (err) {
        showToast('复制失败!', 'error');
      }
    });

    // 添加延迟的复制函数,确保进度条有足够的时间显示
    async function copyToClipboardWithDelay(text) {
      return new Promise((resolve, reject) => {
        setTimeout(async () => {
          try {
            await navigator.clipboard.writeText(text);
            resolve();
          } catch (err) {
            reject(err);
          }
        }, 50); // 添加小延迟,确保进度条动画可见
      });
    }

    // 返回顶部按钮
    window.addEventListener('scroll', function () {
      if (window.pageYOffset > 300) {
        backToTop.classList.add('show');
      } else {
        backToTop.classList.remove('show');
      }
    });

    backToTop.addEventListener('click', function () {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    });

    // 显示带进度条的堆叠式提示信息
    function showToast(message, type = 'success') {
      // 创建新的提示元素
      const toast = document.createElement('div');
      toast.className = `toast ${type}`;
      toast.textContent = message;

      // 创建进度条
      const progressBar = document.createElement('div');
      progressBar.className = 'toast-progress';
      toast.appendChild(progressBar);

      // 将提示元素添加到容器
      toastContainer.appendChild(toast);

      // 使用setTimeout以确保DOM更新并应用过渡效果
      setTimeout(() => {
        toast.classList.add('show');
        // 强制重排以确保动画正确触发
        void toast.offsetWidth;
        // 设置进度条动画
        progressBar.style.transition = 'transform 3s linear';
        progressBar.style.transform = 'scaleX(0)';
      }, 10);

      // 设置自动消失
      setTimeout(() => {
        toast.classList.remove('show');
        setTimeout(() => {
          if (toast.parentNode === toastContainer) {
            toastContainer.removeChild(toast);
          }
        }, 300);
      }, 3000);
    }

    // 转换函数:将拼音标注文本转换为Ruby标签格式
    function convertToRuby(text) {
      // 处理汉字和拼音的组合(中zhōng 文wén)
      let rubyText = text.replace(
        /([\u4e00-\u9fa5])([a-zA-Zāáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜü]+)/g,
        '<ruby><rb>$1</rb><rt>$2</rt></ruby>'
      );

      // 处理带括号的拼音格式(中(zhōng) 文(wén))
      rubyText = rubyText.replace(
        /([\u4e00-\u9fa5])\(([a-zA-Zāáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜü]+)\)/g,
        '<ruby><rb>$1</rb><rt>$2</rt></ruby>'
      );

      // 处理标点符号后的空格
      rubyText = rubyText.replace(/([,。、;:?!])\s+/g, '$1');

      // 保留换行符
      rubyText = rubyText.replace(/\n/g, '<br>');

      return rubyText;
    }

    // 设置Ruby标签的高亮功能
    function setupRubyHighlighting() {
      // 获取所有ruby元素
      const rubyElements = output.querySelectorAll('ruby');

      // 添加右键事件监听
      rubyElements.forEach(ruby => {
        ruby.addEventListener('contextmenu', handleRubyContextMenu);
      });

      // 禁用默认右键菜单
      output.addEventListener('contextmenu', function (e) {
        e.preventDefault();
      });
    }

    // 处理Ruby标签的右键菜单
    function handleRubyContextMenu(e) {
      e.preventDefault();
      e.stopPropagation();

      // 根据选择的内容,处理单个或多个Ruby元素
      let selectedRubyElements = [];

      // 检查是否有选择的文本
      const selection = window.getSelection();

      if (selection.toString().trim() !== '') {
        // 有选择的文本,处理选中范围内的所有Ruby元素
        const range = selection.getRangeAt(0);
        const container = range.commonAncestorContainer;

        // 如果容器是output元素或其子元素
        if (output.contains(container)) {
          // 获取选中范围内的所有Ruby元素
          const rubyElements = output.querySelectorAll('ruby');
          rubyElements.forEach(ruby => {
            if (range.intersectsNode(ruby)) {
              selectedRubyElements.push(ruby);
            }
          });
        }
      } else {
        // 没有选择文本,只处理当前右键点击的Ruby元素
        let currentRuby = e.target;

        // 确保获取到的是ruby元素
        while (currentRuby && currentRuby.tagName !== 'RUBY') {
          if (currentRuby === output) break;
          currentRuby = currentRuby.parentElement;
        }

        if (currentRuby && currentRuby.tagName === 'RUBY') {
          selectedRubyElements.push(currentRuby);
        }
      }

      // 如果有选中的Ruby元素
      if (selectedRubyElements.length > 0) {
        // 显示右键菜单
        contextMenu.style.display = 'block';
        contextMenu.style.left = `${e.clientX}px`;
        contextMenu.style.top = `${e.clientY}px`;

        // 确保菜单不超出视口边界
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        const menuRect = contextMenu.getBoundingClientRect();

        if (e.clientX + menuRect.width > viewportWidth) {
          contextMenu.style.left = `${viewportWidth - menuRect.width - 5}px`;
        }

        if (e.clientY + menuRect.height > viewportHeight) {
          contextMenu.style.top = `${viewportHeight - menuRect.height - 5}px`;
        }

        // 设置高亮菜单项的点击事件
        highlightMenuItem.onclick = function (e) {
          e.stopPropagation();
          // 切换所有选中Ruby元素的高亮状态
          selectedRubyElements.forEach(ruby => {
            ruby.classList.toggle('highlighted');
          });

          // 隐藏右键菜单
          contextMenu.style.display = 'none';
        };
      }
    }

    // 点击页面其他区域时隐藏右键菜单
    document.addEventListener('click', function () {
      contextMenu.style.display = 'none';
    });

    // 窗口大小变化时隐藏右键菜单
    window.addEventListener('resize', function () {
      contextMenu.style.display = 'none';
    });

    // 全局拦截右键事件(确保菜单外的右键行为不受影响)
    document.addEventListener('contextmenu', function (e) {
      // 仅当点击的是右键菜单本身时阻止默认行为
      if (e.target.closest('.context-menu')) {
        e.preventDefault();
        e.stopPropagation();
        return false;
      }
    });
  </script>
</body>

</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2333676.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV 图形API(30)图像滤波-----腐蚀操作函数erode()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 使用特定的结构元素腐蚀图像。 cv::gapi::erode 是 OpenCV 的 G-API 模块中用于执行图像腐蚀操作的函数。腐蚀是一种基本的形态学操作&#xff…

特殊定制版,太给力了!

今天给大家分享一款超棒的免费录屏软件&#xff0c;真的是录屏的好帮手&#xff01; 这款软件功能可以录制 MP4、AVI、WMV 格式的标清、高清、原画视频&#xff0c;满足你各种需求。 云豹录屏大师 多功能录屏神器 它的界面特别简洁&#xff0c;上手超快&#xff0c;用起来很顺…

go:实现最简单区块链

1.新建文件夹命名为blockchain,在此文件夹下分别创建两个文件一个为block.go另一个为chain.go如下图所示: 2.写入代码: block.go package blockchainimport ("bytes""crypto/sha256""encoding/gob""log""strconv""ti…

工业相机使用笔记

目前工业相机有多种分类方式&#xff0c;以下是基于不同原理和特点的类别总结&#xff1a; 按维度分类 2D相机&#xff1a; 原理&#xff1a;通过镜头将二维平面上的物体成像在图像传感器上&#xff0c;传感器上的像素点阵列捕捉物体的光信号&#xff0c;并转换为电信号或数字…

系分论文《论面向服务开发方法在设备租赁行业的应用》

系统分析师论文系列 【摘要】 2022年5月&#xff0c;我司承接某工程机械租赁企业"智能租赁运营管理平台"建设项目&#xff0c;我作为系统分析师主导系统架构设计。该项目需整合8大类2000余台设备资产&#xff0c;覆盖全国15个区域运营中心与300家代理商&#xff0c;实…

04--网络属性设置与多路复用

一、TCP可靠性分析 二、 scoket 属性设置 1、socket 属性设置表 NAMEgetsockopt, setsockopt - get and set options on sockets获取 和 设置 套接字属性 SYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int getsockopt(int so…

AI领域再突破,永洪科技荣获“2025人工智能+创新案例”奖

在2025年的今天&#xff0c;人工智能已从技术概念全面渗透至产业核心。中国作为全球AI技术应用的前沿阵地&#xff0c;正通过“人工智能”行动加速推进技术与实体经济深度融合。 这一背景下&#xff0c;永洪科技凭借其“国内某头部ICT人力资源板块GenAI项目”荣获“2025全国企业…

基于疾风大模型的新能源储能优化系统:方法、实现与案例分析

一、引言 随着可再生能源渗透率不断提高,储能系统在电力系统中的重要性日益凸显。传统储能控制方法主要基于规则策略和简单优化算法,难以应对高比例新能源场景下的复杂决策需求。本文将详细介绍如何利用疾风大模型(Gale Model)构建智能化的新能源储能优化系统,包含核心方…

菊风RTC 2.0 开发者文档正式发布,解锁音视频新体验!

重磅发布&#xff01; 开发者们&#xff0c;菊风实时音视频2.0文档已正式发布上线&#xff0c;为您提供更清晰、更高效的开发支持&#xff01;让菊风实时音视频2.0为您的音视频应用加速~ 菊风实时音视频2.0聚焦性能升级、体验升级、录制服务升级&#xff0c;助力视频通话、语…

OpenCv高阶(一)——图像金字塔(上采样、下采样)

目录 图像金字塔 一、上下采样原理 1、向下取样 2、向上采样 3、图像金字塔的作用 二、案例实现 1、高斯下采样 2、高斯金字塔中的上采样 3、对下采样的结果做上采样&#xff0c;图像变模糊&#xff0c;无法复原 4、拉普拉斯金字塔&#xff08;图片复原&#xff09; 图…

LEARNING DYNAMICS OF LLM FINETUNING【论文阅读笔记】

LEARNING DYNAMICS OF LLM FINETUNING 一句话总结 作者将LLM的学习动力机制拆解成AKG三项&#xff0c;并分别观察了SFT和DPO训练过程中​​正梯度信号​​和​​负梯度信号​​的变化及其带来的影响&#xff0c;并得到以下结论&#xff1a; ​​SFT通过梯度相似性间接提升无关…

数据集 | 沥青路面缺陷目标检测

文章目录 一、数据集概述1. 行业痛点与数据集价值2. 数据集技术规格 二、样本类别详解1. 裂缝 (Crack)2. 裂缝修补 (Crack Repair)3. 坑洞 (Pothole)4. 坑洞修补 (Pothole Repair)5. 井盖 (Manhole Cover)6. 其他 (Other) 三、标注工具四、下载地址 一、数据集概述 1. 行业痛点…

AllData数据中台升级发布 | 支持K8S数据平台2.0版本

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨杭州奥零数据科技官网&#xf…

.net Core 和 .net freamwork 调用 deepseek api 使用流输出文本(对话补全)

.net Core 调用 deepseek api 使用流输出文本 简下面直接上代码&#xff08;.net core&#xff09;&#xff1a;最后再贴一个 .net Freamwork 4 可以用的代码TLS 的代码至关重要的&#xff1a;&#xff08;下面这个&#xff09; 简 在官网里面有许多的案例&#xff1a;我们通过…

springcloud整理

问题1.服务拆分后如何进行服务之间的调用 我们该如何跨服务调用&#xff0c;准确的说&#xff0c;如何在cart-service中获取item-service服务中的提供的商品数据呢&#xff1f; 解决办法&#xff1a;Spring给我们提供了一个RestTemplate的API&#xff0c;可以方便的实现Http请…

04-算法打卡-数组-二分查找-leetcode(69)-第四天

1 题目地址 69. x 的平方根 - 力扣&#xff08;LeetCode&#xff09;69. x 的平方根 - 给你一个非负整数 x &#xff0c;计算并返回 x 的 算术平方根 。由于返回类型是整数&#xff0c;结果只保留 整数部分 &#xff0c;小数部分将被 舍去 。注意&#xff1a;不允许使用任何内…

[Windows] 字体渲染 mactype v2025.4.11

[Windows] 字体渲染 mactype 链接&#xff1a;https://pan.xunlei.com/s/VONeCUP2hEgO5WIQImgtGUmrA1?pwdyruf# 025.4.11 Variable font support 可变字体支持已到来。 本版本将可变字体支持扩展到所有 GDI 应用程序。 所有 win32 程序中的字体&#xff0c;如 Noto Sans、Se…

VSCode CMake调试CPP程序

文章目录 1 安装C与CMake插件2 配置CMakeLists.txt3 使用CMake编译调试3.1 编译3.2 调试 4 自定义构建调试参考 1 安装C与CMake插件 C插件 CMake插件 2 配置CMakeLists.txt 编写测试程序 #include<iostream>int main(int argc, char const *argv[]) {int a 1, b 2;i…

Halo 设置 GitHub - OAuth2 认证指南

在当今数字化时代&#xff0c;用户认证的便捷性和安全性愈发重要。对于使用 Halo 搭建个人博客或网站的开发者而言&#xff0c;引入 GitHub - OAuth2 认证能够极大地提升用户登录体验。今天&#xff0c;我们就来详细探讨一下如何在 Halo 中设置 GitHub - OAuth2 认证。 一、为…

【unity游戏开发——Animator动画】Animator动画状态机复用——重写动画控制器 Animator Override Controller

注意&#xff1a;考虑到UGUI的内容比较多&#xff0c;我将UGUI的内容分开&#xff0c;并全部整合放在【unity游戏开发——Animator动画】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 一、状态机复用是什么&#xff1f;二、实战专栏推荐完结 一、状态机复…