convd 2.6.0+alpha

A profile converter for surge/clash.
Documentation
<!DOCTYPE html>
<!--
  Liquid Glass Calculation Canvas

  This page implements a web version of the innovative calculator app
  described in articles about “有数/Tydlig”.  It provides an infinite
  canvas where each line of math can be edited like a draft.  You can drag
  any result onto subsequent expressions to build up chain calculations.
  When you edit any number, all dependent results update in real time.  The
  design uses translucent “glass blocks” inspired by Apple’s new Liquid
  Glass material【933991152658404†L335-L368】 and the interactive design
  principles of popular calculators that emphasise responsive results and
  draggable numbers【401698968521485†L25-L36】.
-->
<html lang="zh-CN">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>玻璃计算器 – 流式草稿计算</title>
    <link rel="preconnect" href="https://fonts.googleapis.com"/>
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"/>
    <style>
        :root {
          --bg-gradient: linear-gradient(135deg, #4e54c8, #8f94fb);
          --primary: #ffffff;
          --accent: #7f8ff4;
          --header-h: 60px;
        }
        * { box-sizing: border-box; }
        body {
          margin: 0;
          font-family: 'Poppins', sans-serif;
          background: var(--bg-gradient);
          color: var(--primary);
          display: flex;
          flex-direction: column;
          min-height: 100vh;
        }
        header {
          position: fixed;
          top: 0;
          left: 0;
          right: 0;
          height: var(--header-h);
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 0 24px;
          background: rgba(255, 255, 255, 0.1);
          border-bottom: 1px solid rgba(255, 255, 255, 0.3);
          backdrop-filter: blur(12px) saturate(180%);
          -webkit-backdrop-filter: blur(12px) saturate(180%);
          box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
          z-index: 100;
        }
        .logo {
          font-weight: 600;
          font-size: 1.4rem;
          letter-spacing: 1px;
        }
        .hero {
          padding-top: calc(var(--header-h) + 80px);
          padding-bottom: 40px;
          text-align: center;
        }
        .hero h1 { margin: 0; font-size: 2.6rem; font-weight: 600; }
        .hero p { margin-top: 12px; font-size: 1.2rem; opacity: 0.9; max-width: 680px; margin-left:auto; margin-right:auto; }
        /* Canvas container for calculations */
        #canvas {
          width: 100%;
          max-width: 900px;
          margin: 0 auto;
          padding: 0 16px;
          padding-bottom: 60px;
        }
        .calc-row {
          position: relative;
          z-index: 0;
          display: flex;
          align-items: center;
          margin-bottom: 24px;
          padding: 20px 24px;
          background: rgba(255, 255, 255, 0.28);
          border: 1px solid rgba(255, 255, 255, 0.4);
          border-radius: 28px;
          backdrop-filter: blur(16px) saturate(220%);
          -webkit-backdrop-filter: blur(16px) saturate(220%);
          box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25), 0 6px 20px rgba(0, 0, 0, 0.2);
        }
        /* Inner edge highlight for each row */
        .calc-row::before,
        .calc-row::after {
          content: "";
          position: absolute;
          border-radius: inherit;
          pointer-events: none;
        }
        .calc-row::before {
          inset: 0;
          padding: 2px;
          background:
            linear-gradient(145deg, rgba(255,255,255,0.6), rgba(255,255,255,0.1))
            border-box;
          -webkit-mask:
            linear-gradient(#000 0 0) content-box,
            linear-gradient(#000 0 0);
          mask:
            linear-gradient(#000 0 0) content-box,
            linear-gradient(#000 0 0);
          -webkit-mask-composite: xor;
                  mask-composite: exclude;
        }
        .calc-row::after {
          inset: 4px;
          box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.35);
        }
        .expression {
          flex: 1;
          min-height: 32px;
          padding: 6px;
          border: none;
          outline: none;
          font-size: 1.2rem;
          color: var(--primary);
          position: relative;
          z-index: 1;
        }
        .expression:focus {
          outline: none;
        }
        .result {
          margin-left: 20px;
          font-weight: 600;
          font-size: 1.2rem;
          cursor: grab;
          padding: 4px 14px;
          border-radius: 14px;
          background: rgba(255, 255, 255, 0.25);
          border: 1px solid rgba(255, 255, 255, 0.5);
          backdrop-filter: blur(10px) saturate(180%);
          -webkit-backdrop-filter: blur(10px) saturate(180%);
          white-space: nowrap;
          min-width: 48px;
          display: flex;
          align-items: center;
          justify-content: center;
          position: relative;
          z-index: 1;
          /* Dark text for contrast against the light pill */
          color: #1d2275;
        }
        .result.error {
          color: #ff6b6b;
        }
        .ref {
          display: inline-block;
          padding: 2px 4px;
          margin: 0 2px;
          border-radius: 8px;
          background: rgba(255,255,255,0.25);
          border: 1px solid rgba(255,255,255,0.4);
          cursor: pointer;
          user-select: none;
        }
        #addRowBtn {
          display: block;
          margin: 20px auto;
          padding: 10px 26px;
          font-size: 1.1rem;
          font-weight: 600;
          color: var(--primary);
          background: rgba(255, 255, 255, 0.25);
          border: 1px solid rgba(255,255,255,0.45);
          border-radius: 20px;
          cursor: pointer;
          transition: background 0.3s, box-shadow 0.3s;
          backdrop-filter: blur(8px) saturate(180%);
          -webkit-backdrop-filter: blur(8px) saturate(180%);
        }
        #addRowBtn:hover {
          background: rgba(255, 255, 255, 0.35);
          box-shadow: 0 4px 12px rgba(0,0,0,0.3);
        }
    </style>
</head>
<body>
<header>
    <div class="logo">Glass Calc</div>
</header>
<section class="hero">
    <h1>流式草稿计算</h1>
    <p>像写草稿一样进行数学计算,拖拽任意结果参与下一步运算,实时更新关联数字的所有结果。这是向 Tydlig/有数
        致敬的网页版计算体验。</p>
</section>
<div id="canvas"></div>
<button id="addRowBtn">添加表达式</button>

<script>
    (function() {
      const rows = [];
      let nextId = 1;

      const canvas = document.getElementById('canvas');
      const addBtn = document.getElementById('addRowBtn');

      function getRowById(id) {
        return rows.find(r => r.id === Number(id));
      }

      function updateAllRefSpans(refId) {
        const refRows = document.querySelectorAll(`span.ref[data-ref-id="${refId}"]`);
        const row = getRowById(refId);
        if (!row) return;
        refRows.forEach(span => {
          span.textContent = row.value;
        });
      }

      function evaluateRow(row, visited = new Set()) {
        if (visited.has(row.id)) return; // avoid cycles
        visited.add(row.id);
        // Build expression string and collect references
        const exprNodes = Array.from(row.exprEl.childNodes);
        let exprStr = '';
        const newRefs = new Set();
        exprNodes.forEach(node => {
          if (node.nodeType === Node.TEXT_NODE) {
            exprStr += node.textContent;
          } else if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('ref')) {
            const rid = parseInt(node.getAttribute('data-ref-id'));
            newRefs.add(rid);
            const refRow = getRowById(rid);
            const val = refRow ? refRow.value : 0;
            exprStr += '(' + val + ')';
          }
        });
        // Update dependency graph
        // Remove dependents from old references
        row.references.forEach(rid => {
          if (!newRefs.has(rid)) {
            const refRow = getRowById(rid);
            if (refRow) refRow.dependents.delete(row.id);
          }
        });
        // Add dependents to new references
        newRefs.forEach(rid => {
          if (!row.references.has(rid)) {
            const refRow = getRowById(rid);
            if (refRow) refRow.dependents.add(row.id);
          }
        });
        row.references = newRefs;
        // Evaluate expression
        try {
          let result;
          if (exprStr.trim() === '') {
            // Empty expression returns zero; display blank to avoid "undefined"
            result = 0;
          } else {
            // Provide Math scope to expression
            const scope = Object.create(Math);
            const fn = new Function('with(this){ return ' + exprStr + '}');
            result = fn.call(scope);
          }
          row.value = result;
          // Display result only if expression is not empty
          if (exprStr.trim() === '') {
            row.resultEl.textContent = '';
          } else {
            row.resultEl.textContent = String(result);
          }
          row.resultEl.classList.remove('error');
        } catch (e) {
          row.value = NaN;
          row.resultEl.textContent = '错误';
          row.resultEl.classList.add('error');
        }
        // Update displayed ref spans for this row
        updateAllRefSpans(row.id);
        // Recompute dependents
        row.dependents.forEach(did => {
          const depRow = getRowById(did);
          if (depRow) evaluateRow(depRow, visited);
        });
      }

      function insertRefSpan(exprEl, refId, value) {
        const sel = window.getSelection();
        if (!sel.rangeCount) return;
        const range = sel.getRangeAt(0);
        range.deleteContents();
        const span = document.createElement('span');
        span.className = 'ref';
        span.setAttribute('data-ref-id', refId);
        span.contentEditable = false;
        span.textContent = value;
        range.insertNode(span);
        // place caret after inserted span
        const after = document.createTextNode('');
        span.after(after);
        sel.removeAllRanges();
        const newRange = document.createRange();
        newRange.setStart(after, 0);
        newRange.setEnd(after, 0);
        sel.addRange(newRange);
      }

      function setupRowEvents(row) {
        // Evaluate expression whenever the content changes.  Some browsers
        // emit keyup instead of input for contenteditable elements, so we
        // listen to both.
        const evaluate = () => evaluateRow(row);
        // Some environments do not reliably emit input/key events for programmatic
        // text insertion (e.g. via automation).  To ensure calculations update
        // consistently, we listen to input, keyup and keydown.
        row.exprEl.addEventListener('input', evaluate);
        row.exprEl.addEventListener('keyup', evaluate);
        row.exprEl.addEventListener('keydown', evaluate);
        row.exprEl.addEventListener('dragover', ev => {
          ev.preventDefault();
        });
        row.exprEl.addEventListener('drop', ev => {
          ev.preventDefault();
          const refId = ev.dataTransfer.getData('text/ref');
          if (!refId) return;
          const refRow = getRowById(refId);
          if (!refRow) return;
          insertRefSpan(row.exprEl, refId, refRow.value);
          evaluateRow(row);
        });
        row.resultEl.addEventListener('dragstart', ev => {
          ev.dataTransfer.setData('text/ref', row.id);
          ev.dataTransfer.effectAllowed = 'copy';
        });
      }

      function createRow() {
        const id = nextId++;
        const rowEl = document.createElement('div');
        rowEl.className = 'calc-row';
        const expr = document.createElement('div');
        expr.className = 'expression';
        expr.contentEditable = true;
        expr.spellcheck = false;
        expr.setAttribute('placeholder', '输入表达式…');
        const result = document.createElement('div');
        result.className = 'result';
        result.draggable = true;
        rowEl.appendChild(expr);
        rowEl.appendChild(result);
        canvas.appendChild(rowEl);
        const row = { id, el: rowEl, exprEl: expr, resultEl: result, references: new Set(), dependents: new Set(), value: 0 };
        rows.push(row);
        setupRowEvents(row);
        evaluateRow(row);
        // Focus newly created row
        setTimeout(() => {
          expr.focus();
        }, 0);
        return row;
      }

      addBtn.addEventListener('click', () => {
        createRow();
      });
      // Create first row on load
      createRow();
    })();
</script>
</body>
</html>