<%_*
// RLM template v2. Protect code before considering line direction.
const editor = app.workspace.activeEditor?.editor ?? app.workspace.activeLeaf?.view?.editor;
if (!editor) { tR = ""; return; }

const RLM = "\u200F";

const lineCount = typeof editor.lineCount === "function"
  ? editor.lineCount()
  : (typeof editor.lastLine === "function" ? editor.lastLine() + 1 : 0);

if (lineCount === 0) { tR = ""; return; }

const getLine = (n) => editor.getLine(n) ?? "";
const clampLine = (n) => Math.max(0, Math.min(n, lineCount - 1));

const consumeSpaces = (s, i) => { while (i < s.length && (s[i] === " " || s[i] === "\t")) i++; return i; };

function computeInsertCh(line) {
  let i = 0;
  i = consumeSpaces(line, i);

  while (line[i] === ">") {
    i++;
    i = consumeSpaces(line, i);
  }

  // Headings: ### ...
  let j = i, hashes = 0;
  while (j < line.length && line[j] === "#" && hashes < 6) { j++; hashes++; }
  if (hashes > 0 && /[ \t]/.test(line[j] ?? "")) {
    i = consumeSpaces(line, j);
    return i;
  }

  // Unordered list: - / * / +
  if ((line[i] === "-" || line[i] === "*" || line[i] === "+") && line[i + 1] === " ") {
    i = consumeSpaces(line, i + 2);

    // Task list: - [ ] / - [x]
    const a = line[i], b = (line[i + 1] || "").toLowerCase(), c = line[i + 2];
    if (a === "[" && (b === " " || b === "x") && c === "]") {
      i = i + 3;
      if (line[i] === " ") i = consumeSpaces(line, i + 1);
    }
    return i;
  }

  // Ordered list: 1. / 1)
  let k = i;
  while (k < line.length && /[0-9]/.test(line[k])) k++;
  if (k > i && (line[k] === "." || line[k] === ")") && line[k + 1] === " ") {
    i = consumeSpaces(line, k + 2);
    return i;
  }

  return i;
}

const isHebrew = (cp) => cp >= 0x0590 && cp <= 0x05FF;
const isArabicScript = (cp) =>
  (cp >= 0x0600 && cp <= 0x06FF) ||
  (cp >= 0x0750 && cp <= 0x077F) ||
  (cp >= 0x08A0 && cp <= 0x08FF) ||
  (cp >= 0xFB50 && cp <= 0xFDFF) ||
  (cp >= 0xFE70 && cp <= 0xFEFF);

const isPersianDigit = (cp) => cp >= 0x06F0 && cp <= 0x06F9; // ۰..۹
const isArabicIndicDigit = (cp) => cp >= 0x0660 && cp <= 0x0669; // ٠..٩

function firstStrongIsLTR(line, startCh) {
  let idx = Math.max(0, startCh);

  while (idx < line.length) {
    const cp = line.codePointAt(idx);
    const ch = String.fromCodePoint(cp);

    // Skip bidi marks
    if (cp === 0x200F || cp === 0x200E || cp === 0x061C) { idx += ch.length; continue; }

    // Skip whitespace
    if (/\s/u.test(ch)) { idx += ch.length; continue; }

    // Skip punctuation/symbols
    if (/[\-_*#>\[\]\(\)`"'.,:;!?@$/\\|~^+=]/u.test(ch)) { idx += ch.length; continue; }

    // RTL strong
    if (/[\p{P}\p{S}]/u.test(ch)) { idx += ch.length; continue; }

    if (isHebrew(cp) || isArabicScript(cp) || isPersianDigit(cp) || isArabicIndicDigit(cp)) return false;

    // LTR strong
    if (/\p{Script=Latin}/u.test(ch) || /[0-9]/.test(ch)) return true;

    idx += ch.length;
  }

  return false;
}

function stripForBlockCheck(line) {
  let i = 0;
  i = consumeSpaces(line, i);
  while (line[i] === ">") { i++; i = consumeSpaces(line, i); }
  return line.slice(i);
}

function isHorizontalRule(line) {
  const s = stripForBlockCheck(line).trim();
  return /^(---+|\*\*\*+|___+)$/.test(s);
}

function isTableLine(line) {
  const s = stripForBlockCheck(line);
  return s.startsWith("|");
}

// --- Table support: insert an RLM at the start of each LTR-leading cell ---
// Returns { newText, positions } (positions = insert offsets in the ORIGINAL
// line, used for cursor accounting), or null when nothing changes.
function fixTableLine(text) {
  let p = consumeSpaces(text, 0);
  while (text[p] === ">") { p++; p = consumeSpaces(text, p); }
  const prefix = text.slice(0, p);
  const content = text.slice(p);

  // Split on unescaped pipes; cells.join("|") reconstructs `content` exactly.
  const cells = [];
  let buf = "", i = 0;
  while (i < content.length) {
    const ch = content[i];
    if (ch === "\\" && i + 1 < content.length) { buf += ch + content[i + 1]; i += 2; continue; }
    if (ch === "|") { cells.push(buf); buf = ""; i++; continue; }
    buf += ch; i++;
  }
  cells.push(buf);

  // Skip the separator row (|---|:--:|...)
  const meaningful = cells.map((c) => c.trim()).filter((c) => c.length);
  if (meaningful.length > 0 && meaningful.every((c) => /^:?-+:?$/.test(c))) return null;

  const positions = [];
  const newCells = [];
  let absBase = prefix.length; // start offset of the current cell in the original line
  let anyChange = false;
  for (let ci = 0; ci < cells.length; ci++) {
    const cell = cells[ci];
    const pos = consumeSpaces(cell, 0);
    if (pos < cell.length && cell[pos] !== RLM && firstStrongIsLTR(cell, pos)) {
      newCells.push(cell.slice(0, pos) + RLM + cell.slice(pos));
      positions.push(absBase + pos);
      anyChange = true;
    } else {
      newCells.push(cell);
    }
    absBase += cell.length + 1; // +1 for the "|" removed by the split
  }

  if (!anyChange) return null;
  return { newText: prefix + newCells.join("|"), positions };
}

function detectFrontmatterEnd() {
  if (lineCount === 0) return -1;
  const first = getLine(0).trim();
  if (first !== "---") return -1;
  for (let i = 1; i < lineCount; i++) {
    const t = getLine(i).trim();
    if (t === "---" || t === "...") return i;
  }
  return lineCount - 1;
}

// Conservative Markdown guard, not a replacement for Obsidian's parser.
// Keep quote indentation: consuming all spaces after ">" hides indented code.
function blockContent(raw) {
  let rest = raw, depth = 0;
  while (/^ {0,3}>/.test(rest)) {
    rest = rest.replace(/^ {0,3}>[ \t]?/, "");
    depth++;
  }
  // Recognize fences immediately inside list markers too. Ordinary deeply
  // indented list text is deliberately skipped rather than risking code edits.
  const content = rest.replace(/^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(?:\[[ xX]\][ \t]+)?/, "");
  return { rest, content, depth };
}

function buildProtectedSet(maxLine) {
  const protectedLines = new Set();
  const frontEnd = detectFrontmatterEnd();
  for (let i = 0; i <= Math.min(frontEnd, maxLine); i++) protectedLines.add(i);
  let fence = null, mathDepth = null, html = null;
  for (let i = 0; i <= maxLine; i++) {
    if (protectedLines.has(i)) continue;
    const raw = getLine(i);
    const { rest, content, depth } = blockContent(raw);
    // A quoted block ends when its container ends. Do not swallow later prose.
    if (fence && depth < fence.depth && raw.trim()) fence = null;
    if (mathDepth !== null && depth < mathDepth && raw.trim()) mathDepth = null;
    if (html && depth < html.depth && raw.trim()) html = null;
    if (fence) {
      protectedLines.add(i);
      const close = content.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
      if (close && close[1][0] === fence.char && close[1].length >= fence.len) fence = null;
      continue;
    }
    if (mathDepth !== null) {
      protectedLines.add(i);
      if (/^\s*\$\$[ \t]*$/.test(content)) mathDepth = null;
      continue;
    }
    if (html) {
      if (html.blank && !content.trim()) { html = null; continue; }
      protectedLines.add(i);
      if (html.end && html.end.test(content)) html = null;
      continue;
    }
    // Both space-indented and tab-indented code stay byte-for-byte unchanged.
    if (/^(?: {4}| {0,3}\t)/.test(rest)) { protectedLines.add(i); continue; }
    const open = content.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
    if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
      protectedLines.add(i);
      fence = { char: open[1][0], len: open[1].length, depth };
      continue;
    }
    if (/^\s*\$\$.+\$\$[ \t]*$/.test(content)) { protectedLines.add(i); continue; }
    if (/^\s*\$\$[ \t]*$/.test(content)) {
      protectedLines.add(i); mathDepth = depth; continue;
    }
    // Raw HTML/comment bodies are not prose. A blank-line boundary is used
    // conservatively for other HTML starts, including custom Obsidian markup.
    const trimmed = content.trimStart();
    if (/^<(?:[A-Za-z/!?])/.test(trimmed)) {
      protectedLines.add(i);
      let end = null;
      if (trimmed.startsWith("<!--")) end = /-->/;
      else if (trimmed.startsWith("<![CDATA[")) end = /\]\]>/;
      else if (trimmed.startsWith("<?")) end = /\?>/;
      else {
        const tag = trimmed.match(/^<(script|style|pre|textarea)(?:[ \t>])/i);
        if (tag) end = new RegExp("</" + tag[1] + "[ \\t]*>", "i");
      }
      if (!end || !end.test(trimmed)) html = { depth, end, blank: !end };
      continue;
    }
    if (isHorizontalRule(raw) || /^ {0,3}(?:[-*_][ \t]*){3,}$/.test(rest)) protectedLines.add(i);
    // Link-reference definitions must start with their label, not an RLM.
    if (/^ {0,3}\[[^\]]+\]:/.test(rest)) protectedLines.add(i);
  }
  return protectedLines;
}

function getRange() {
  const sel = (editor.listSelections?.() ?? [])[0];
  if (!sel) {
    const cur = editor.getCursor();
    return { start: cur.line, end: cur.line, cursor: cur, hasSelection: false };
  }

  const { anchor, head } = sel;
  const hasSelection = !(anchor.line === head.line && anchor.ch === head.ch);
  const start = hasSelection ? Math.min(anchor.line, head.line) : editor.getCursor().line;
  const later = anchor.line > head.line || (anchor.line === head.line && anchor.ch > head.ch) ? anchor : head;
  // End positions are exclusive. Selecting up to column zero of the next line
  // must not modify that unselected line (also for backwards selections).
  const end = hasSelection ? later.line - (later.line > start && later.ch === 0 ? 1 : 0) : editor.getCursor().line;

  const cursor = hasSelection ? head : editor.getCursor();
  return { start, end, cursor, hasSelection };
}

const { start, end, cursor } = getRange();
const startLine = clampLine(start);
const endLine = clampLine(end);

const protectedLines = buildProtectedSet(endLine);

// Collapse selection to avoid Templater replacing the selection with template output
editor.setCursor(cursor);

let cursorShift = 0;

for (let lineNo = endLine; lineNo >= startLine; lineNo--) {
  if (protectedLines.has(lineNo)) continue;

  const text = getLine(lineNo);
  if (!text.trim()) continue;

  // Table line: fix each cell, replacing the whole line.
  if (isTableLine(text)) {
    const res = fixTableLine(text);
    if (res) {
      editor.replaceRange(res.newText, { line: lineNo, ch: 0 }, { line: lineNo, ch: text.length });
      if (lineNo === cursor.line) {
        for (const pos of res.positions) if (cursor.ch >= pos) cursorShift++;
      }
    }
    continue;
  }

  const insertCh = computeInsertCh(text);
  if (text[insertCh] === RLM) continue;

  if (firstStrongIsLTR(text, insertCh)) {
    editor.replaceRange(RLM, { line: lineNo, ch: insertCh });

    if (lineNo === cursor.line && cursor.ch >= insertCh) {
      cursorShift += 1;
    }
  }
}

editor.setCursor({ line: cursor.line, ch: cursor.ch + cursorShift });
tR = "";
_%>