Insert Multiple column callouts throught templater wizard

I always liked callout solutions from MCC pack. However I rarely used it, because I was forgetting the syntax and was too lazy to take a look in the .css file.

Today I decided to make a templater script that will insert any callout I want, including floating and multicolumn options from the pack.

<%*
/*
Prereqs: MCC Multi Column CSS pack + Templater plugin; this wizard inserts regular, float, and multi‑column callouts with pipe‑style metadata. 
Workflow: pick callout type → (for multi) choose parent metadata → lock a width family → pick per‑column value → then choose callout names; float picks side/size first. 
Tip: Replace the default callout list below with your own (e.g., from ~/.obsidian/plugins/callout-manager/data.json) if using custom callouts. 
*/

// ---------- Default Obsidian callouts (replace with your own if desired) ----------
const callouts = [
  "note","abstract","info","todo","tip","success","question",
  "warning","failure","danger","bug","example","quote","summary"
]; // replace with custom names if using a snippet/plugin that defines them (see note above). [web:24]

// ---------- Suggester helper ----------
const choose = async (labels, values, placeholder) =>
  await tp.system.suggester(labels, values, false, placeholder); // Templater suggester. [web:76]

// ---------- Percentage map and dynamic filtering (pw family) ----------
const pwMap = { pw1:10, pw2:18, pw3:28, pw4:38, pw5:48, pw6:58, pw7:68, pw8:78, pw9:88 }; // from MCC width tokens. [web:24]
const PW_BUDGET = 88; // leave headroom to avoid overflow in wrapped layouts. [web:24]
function allowedPwKeys(remaining){ return Object.entries(pwMap).filter(([k,p])=>p<=remaining).map(([k])=>k); } // [web:24]
function pwLabel(k){ return `${k} — about ${pwMap[k]}% width`; } // [web:24]
function sumPwTokens(tokens){ return tokens.filter(t=>t.startsWith("pw")).reduce((s,t)=>s+(pwMap[t]||0),0); } // [web:24]

// ---------- Single-choice width pickers (family lock uses these) ----------
async function pickWide(colIdx){
  return await choose(
    ["wide-2 — grows 2×","wide-3 — grows 3×","wide-4 — grows 4×","wide-5 — grows 5×"],
    ["wide-2","wide-3","wide-4","wide-5"],
    `Column ${colIdx}: choose wide value`
  ); // wide-x maps to flex-grow in MCC. [web:24]
}
async function pickDw(colIdx, partsSoFar){
  const labels = Array.from({length:8},(_,i)=>`${i+2} parts (dw${i+2}) — parts so far: ${partsSoFar}`);
  const vals   = Array.from({length:8},(_,i)=>`dw${i+2}`);
  return await choose(labels, vals, `Column ${colIdx}: choose parts (dwN)`); // dwN maps to discrete flex ratios. [web:24]
}
async function pickPw(colIdx, usedPwTotalSoFar){
  const remaining = Math.max(0, PW_BUDGET - usedPwTotalSoFar);
  const keys = allowedPwKeys(remaining);
  if (keys.length === 0) {
    await choose([`No percentage options left (remaining ≈${remaining}%) — press ENTER`],[null],`Column ${colIdx}: pw`);
    return null;
  }
  return await choose(keys.map(k=>pwLabel(k)), keys, `Column ${colIdx}: choose percentage (remaining ≈${remaining}%)`); // pwN basis steps. [web:24]
}
async function pickFw(colIdx){
  const pairs = [
    ["fw1 — 100px","fw1"],["fw2 — 200px","fw2"],["fw3 — 300px","fw3"],
    ["fw4 — 400px","fw4"],["fw5 — 500px","fw5"],["fw6 — 600px","fw6"],
    ["fw7 — 700px","fw7"],["fw8 — 800px","fw8"],["fw9 — 900px","fw9"]
  ];
  return await choose(pairs.map(x=>x[0]), pairs.map(x=>x[1]), `Column ${colIdx}: choose fixed width (fwN)`); // fwN fixed basis. [web:24]
}

// ---------- Header helper ----------
const header = (level, kind, metaPipe, title) => {
  const t = title && title.trim() ? `+ ${title.trim()}` : "";
  const chev = level === 2 ? ">>" : ">";
  return `${chev} [!${kind}${metaPipe}]${t}`; // pipe-style metadata per MCC docs. [web:24]
};

// ---------- Top-level ----------
const type = await choose(
  [
    "regular — single custom callout",
    "multi column — [!multi-column] with children",
    "float — aside left/right",
    "blank — minimal container"
  ],
  ["regular","multi","float","blank"],
  "Callout type"
); // primary modes covered by MCC + float. [web:24]

// ---------- Regular ----------
if (type === "regular") {
  const kind = await choose(callouts, callouts, "Regular callout type"); // default set. [web:24]
  const title = await tp.system.prompt("Title (optional)", ""); // no special metadata for regular here. [web:24]
  tR += `${header(1, kind, "", title)}\n> \n`; // emit block. [web:24]
}

// ---------- Multi-column ----------
if (type === "multi") {
  // Parent metadata: one width/flow + optional flex-h; pipes in output. [web:24]
  const optPairs = [
    ["none — standard flow","none"],
    ["center-fixed — fixed width, centered","center-fixed"],
    ["left-fixed — fixed width, left","left-fixed"],
    ["right-fixed — fixed width, right","right-fixed"],
    ["no-wrap — single row with scrollbar (disables width controls)","no-wrap"],
    ["flex-h — children keep natural height (combine with one above)","flex-h"]
  ];
  let picked = [];
  while (true) {
    const labels = optPairs.map(([lbl,val]) => picked.includes(val) ? `✓ ${lbl}` : lbl);
    const vals   = optPairs.map(([,val])=>val);
    const choice = await choose(labels, vals, "Parent metadata — pick width/flow and optionally add flex-h; ESC to finish");
    if (!choice) break;
    if (choice === "none") { picked = []; break; }
    if (choice === "flex-h") {
      if (!picked.includes("flex-h")) picked.push("flex-h");
    } else {
      picked = picked.filter(x => !["center-fixed","left-fixed","right-fixed","no-wrap"].includes(x));
      if (!picked.includes(choice)) picked.push(choice);
    }
  }
  const parentPipe = picked.length ? `|${picked.join("|")}` : ""; // pipe separators. [web:24]
  const parentHasNoWrap = picked.includes("no-wrap"); // width controls invalid under no-wrap per docs. [web:24]

  const nCols = parseInt(await choose(["2","3","4","5"],["2","3","4","5"],"Number of columns")); // practical range. [web:24]

  // Family lock (skip when no-wrap). [web:24]
  let family = "none";
  if (!parentHasNoWrap) {
    family = await choose(
      [
        "none — no width metadata for columns",
        "wide — relative growth",
        "dw — proportional parts",
        "pw — percentage targets",
        "fw — fixed pixels"
      ],
      ["none","wide","dw","pw","fw"],
      "Choose one width family for all columns"
    );
  }

  let out = `${header(1, "multi-column", parentPipe, "")}\n`;

  // Track running totals for pw/dw hints. [web:24]
  let usedPwTotal = 0;
  let dwPartsSoFar = 0;

  for (let i = 0; i < nCols; i++) {
    if (i > 0) out += ">\n";

    // WIDTH FIRST per column (unless no-wrap or family none). [web:24]
    let colTokens = [];
    if (!parentHasNoWrap && family !== "none") {
      if (family === "wide") {
        const tok = await pickWide(i+1); if (tok) colTokens.push(tok);
      } else if (family === "dw") {
        const tok = await pickDw(i+1, dwPartsSoFar);
        if (tok) { colTokens.push(tok); dwPartsSoFar += parseInt(tok.replace("dw","")) || 0; }
      } else if (family === "pw") {
        const tok = await pickPw(i+1, usedPwTotal);
        if (tok) { colTokens.push(tok); usedPwTotal += pwMap[tok] || 0; }
      } else if (family === "fw") {
        const tok = await pickFw(i+1); if (tok) colTokens.push(tok);
      }
    }
    const metaPipe = colTokens.length ? `|${colTokens.join("|")}` : ""; // pipe join. [web:24]

    // THEN column callout type (regular or blank); blank never prompts title. [web:24]
    const ctype = await choose(["regular child","blank"],["regular","blank"],`Column ${i+1}: content type`);
    const kind = ctype === "regular"
      ? await choose(callouts, callouts, `Column ${i+1}: regular type`)
      : "blank";
    const title = kind === "blank" ? "" : await tp.system.prompt(`Column ${i+1} title (optional)`, "");

    out += `${header(2, kind, metaPipe, title)}\n`;
    out += `>> \n`;
  }

  tR += out;
}

// ---------- Float ----------
if (type === "float") {
  // Choose width BEFORE callout kind. [web:24]
  const sideMode = await choose(["Reading mode (left/right)","Live Preview (float-left/float-right)"],["lr","flr"],"Float side mode");
  const sideList = sideMode === "flr" ? ["float-left","float-right"] : ["left","right"];
  const side = await choose(sideList, sideList, "Side");

  const widthPath = await choose(["Preset size — small/medium/large","Granular — choose width metadata"],["preset","granular"],"Float width mode");

  let metaTokens = [];
  if (widthPath === "preset") {
    const size = await choose(["small — about 300px","medium — about 400px","large — about 600px"],["small","medium","large"],"Preset size");
    metaTokens.push(`${side}-${size}`); // legacy preset sizes align with MCC float vars. [web:24]
  } else {
    metaTokens.push(side);
    const fam = await choose(["wide","dw","pw","fw","none"],["wide","dw","pw","fw","none"],"Float: choose width type");
    let tok = null;
    if (fam === "wide") tok = await pickWide("float");
    else if (fam === "dw") tok = await pickDw("float", 0);
    else if (fam === "pw") tok = await pickPw("float", 0);
    else if (fam === "fw") tok = await pickFw("float");
    if (tok && tok !== "none") metaTokens.push(tok);
  }
  const metaPipe = metaTokens.length ? `|${metaTokens.join("|")}` : ""; // [web:24]

  // THEN callout kind; blank has no title prompt. [web:24]
  const contentType = await choose(["regular","blank"],["regular","blank"],"Content type");
  const kind = contentType === "regular"
    ? await choose(callouts, callouts, "Regular callout type")
    : "blank";
  const title = kind === "blank" ? "" : await tp.system.prompt("Title (optional)", "");

  tR += `${header(1, kind, metaPipe, title)}\n> \n`;
}

// ---------- Blank ----------
if (type === "blank") {
  tR += `${header(1, "blank", "", "")}\n> \n`; // standalone blank; no title. [web:24]
}
%>

This is not the perfect and exhaustive version, but it covers all of my use cases and a little more. I think it should be sufficient for the most part of users, but I’d be glad to see how you further tweak it.

1 Like