Code Builder Page
";
const LL_SCRIPT_CLOSE = "";
const LL_CLOSE_SCRIPT_RE = new RegExp("<" + "/" + "script>", "gi");
const LL_CLOSE_SCRIPT_SAFE = "<" + "/" + "script>";
const LL_MAJOR_SCALES = {
"C": ["C","D","E","F","G","A","B"],
"G": ["G","A","B","C","D","E","F#"],
"D": ["D","E","F#","G","A","B","C#"],
"A": ["A","B","C#","D","E","F#","G#"],
"E": ["E","F#","G#","A","B","C#","D#"],
"B": ["B","C#","D#","E","F#","G#","A#"],
"F#": ["F#","G#","A#","B","C#","D#","E#"],
"F": ["F","G","A","Bb","C","D","E"],
"Bb": ["Bb","C","D","Eb","F","G","A"],
"Eb": ["Eb","F","G","Ab","Bb","C","D"],
"Ab": ["Ab","Bb","C","Db","Eb","F","G"],
"Db": ["Db","Eb","F","Gb","Ab","Bb","C"]
};
function llTitleCase_(s){
const txt = String(s || "").trim();
if (!txt) return "";
return txt
.replace(/\s+/g, " ")
.split(" ")
.map(w => {
if (!w) return w;
if (/^[A-Z0-9/]+$/.test(w) && w.length <= 5) return w;
return w[0].toUpperCase() + w.slice(1);
})
.join(" ");
}
function llUnwrapParens_(s){
const t = String(s || "").trim();
const m = t.match(/^\(\s*(.+?)\s*\)$/);
return m ? { inner: m[1], wrapped: true } : { inner: t, wrapped: false };
}
function llWrapMaybe_(s, wrapped){ return wrapped ? "(" + s + ")" : s; }
function llParseChordToken(token) {
token = (token || "").trim();
if (!token) return null;
const { inner } = llUnwrapParens_(token);
token = inner;
let root = token;
let bass = null;
const slashIndex = token.indexOf("/");
if (slashIndex !== -1) {
root = token.slice(0, slashIndex);
bass = token.slice(slashIndex + 1);
}
function splitPart(part) {
const match = part.match(/^([1-7])(.+)?$/);
if (!match) return null;
return { degree: parseInt(match[1], 10), quality: match[2] || "" };
}
const rootInfo = splitPart(root);
if (!rootInfo) return null;
const bassInfo = bass ? splitPart(bass) : null;
return { root: rootInfo, bass: bassInfo };
}
function llNumberToChord(token, key) {
const scale = LL_MAJOR_SCALES[key];
if (!scale) return token;
const { inner, wrapped } = llUnwrapParens_(token);
const parsed = llParseChordToken(inner);
if (!parsed) return llWrapMaybe_(inner, wrapped);
const rootNote = scale[parsed.root.degree - 1] || "?";
let chord = rootNote + (parsed.root.quality || "");
if (parsed.bass) {
const bassNote = scale[parsed.bass.degree - 1] || "?";
chord += "/" + bassNote;
}
return llWrapMaybe_(chord, wrapped);
}
function llCanonicalToChordCanonical(text, key) {
return text.replace(/\[([^\]]+)\]/g, (match, token) => "[" + llNumberToChord(token.trim(), key) + "]");
}
function llLooksLikeChordLine_(line){
const t = String(line || "").trim();
if (!t) return false;
const chordToken = /(?:^|\s)([A-G](?:#|b)?)(?:maj|min|m|sus|dim|aug|add)?\d*(?:\([^\)]*\))?(?:\/[A-G](?:#|b)?)?(?=\s|$)/gi;
let count = 0;
while (chordToken.exec(t)) count++;
return count >= 2;
}
function llIsSectionHeaderLine_(line){
const t = (line || "").trim();
if (!t) return false;
if (t.includes("[") || t.includes("]")) return false;
if (/^(Song Title|Key|Tempo|BPM|Meter|Songwriter)\s*:/i.test(t)) return false;
if (llLooksLikeChordLine_(t)) return false;
if (/^(VERSE|CHORUS|BRIDGE|INTRO|OUTRO|TAG|REFRAIN|PRE[- ]?CHORUS|INTERLUDE|INSTRUMENTAL|TURNAROUND)(\s+\d+)?(\s*\(.*\))?$/i.test(t)) {
return true;
}
if (/^[A-Z0-9][A-Z0-9 \-()]{2,}$/.test(t)) {
const words = t.replace(/[()\-]/g, " ").split(/\s+/).filter(Boolean);
const hasWordyToken = words.some(w => /^[A-Z]{3,}$/.test(w));
return hasWordyToken;
}
return false;
}
function llEscapeHtml_(s) {
return String(s).replace(/[&<>"']/g, (m) => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[m]));
}
function llBuildRichPreHtml_(plainText){
const lines = String(plainText || "").replace(/\r\n/g, "\n").split("\n");
return lines.map(line => {
const esc = llEscapeHtml_(line);
if (llIsSectionHeaderLine_(line)) return '' + esc + '';
return esc;
}).join("\n");
}
function llRenderOverLyrics(text, mode, key) {
const lines = text.replace(/\r\n/g, "\n").split("\n");
const outLines = [];
lines.forEach(line => {
const trimmed = line.trim();
if (!line.includes("[")) {
outLines.push(trimmed === "" ? "" : trimmed);
return;
}
let lyricsLine = "";
const chordChars = [];
function placeChordAt(pos, token) {
if (!token) return;
const chordSymbol = (mode === "chords") ? llNumberToChord(token, key) : token;
while (chordChars.length <= pos) chordChars.push(" ");
for (let i = 0; i < chordSymbol.length; i++) chordChars[pos + i] = chordSymbol[i];
}
let i = 0;
while (i < line.length) {
const ch = line[i];
if (ch === "[") {
const end = line.indexOf("]", i + 1);
if (end !== -1) {
const token = line.slice(i + 1, end).trim();
const pos = Math.max(lyricsLine.length, 0);
placeChordAt(pos, token);
i = end + 1;
continue;
}
}
lyricsLine += ch;
while (chordChars.length < lyricsLine.length) chordChars.push(" ");
i++;
}
const chordLine = chordChars.join("").replace(/\s+$/, "");
if (chordLine.trim()) {
outLines.push(chordLine);
outLines.push(lyricsLine.replace(/\s+$/, ""));
} else {
outLines.push(lyricsLine.replace(/\s+$/, ""));
}
});
return outLines.join("\n");
}
function llLyricsOnly_(text){
const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
const out = lines.map(line => {
if (!line.includes("[")) return line.replace(/\s+$/,"");
const stripped = line.replace(/\[[^\]]*\]/g, "");
return stripped.replace(/\s+$/,"").replace(/[ \t]{2,}/g, " ");
});
return out.join("\n").trimEnd();
}
/* =========================================================
CODE BUILDER APP (Converter UI + Embed Generator)
========================================================= */
function llInitCodeBuilderApp() {
const mount = document.getElementById("ll-codebuilder-app");
if (!mount) return;
// guard against double mount (Squarespace ajax + observers)
if (mount.dataset.llMounted === "1") return;
mount.dataset.llMounted = "1";
const wrapper = document.createElement("div");
wrapper.className = "llc llc-card";
const BRAND_URL = "https://livingliturgy.online/";
const TOOL_URL = "https://livingliturgy.online/free-numbers-chord-chart-converter";
wrapper.innerHTML = `
`;
mount.innerHTML = "";
mount.appendChild(wrapper);
const metaEl = wrapper.querySelector("#llc-meta");
const inputEl = wrapper.querySelector("#llc-input");
const outputEl = wrapper.querySelector("#llc-output");
const statusEl = wrapper.querySelector("#llc-status");
const keySel = wrapper.querySelector("#llc-key");
const copyBtn = wrapper.querySelector("#llc-copy");
const printBtn = wrapper.querySelector("#llc-print");
const outCtxEl = wrapper.querySelector("#llc-out-context");
const genSongBtn = wrapper.querySelector("#llc-generate-song");
const embedBox = wrapper.querySelector("#llc-embed");
const embedTa = wrapper.querySelector("#llc-embed-ta");
const embedCopy = wrapper.querySelector("#llc-embed-copy");
const embedHide = wrapper.querySelector("#llc-embed-hide");
const embedStatus= wrapper.querySelector("#llc-embed-status");
const titleBoxEl = wrapper.querySelector("#llc-title-box");
const titleBoxTitleEl = wrapper.querySelector("#llc-titlebox-title");
const titleBoxWriterEl = wrapper.querySelector("#llc-titlebox-writer");
const titleBoxMetaEl = wrapper.querySelector("#llc-titlebox-meta");
function addField(label, placeholder, type="text") {
const wrap = document.createElement("div");
wrap.className = "llc-field";
wrap.innerHTML = `
`;
metaEl.appendChild(wrap);
return wrap.querySelector("input");
}
const titleInput = addField("Song Title", "e.g., This Is Jesus");
const writerInput = addField("Songwriter", "e.g., Nathan Lain");
const tempoInput = addField("Tempo / Feel", "e.g., Steady, reflective");
const bpmInput = addField("BPM", "e.g., 72", "number");
const meterInput = addField("Meter", "e.g., 4/4");
Object.keys(LL_MAJOR_SCALES).forEach(k => {
const opt = document.createElement("option");
opt.value = k;
opt.textContent = k;
if (k === "E") opt.selected = true;
keySel.appendChild(opt);
});
const SAMPLE =
`VERSE 1
[1]Holy, holy, holy — [4]Lord God Almighty
[6m]Early in the morning [5]our song shall rise to You
CHORUS
[4]Holy, holy, holy — [1]merciful and mighty`;
inputEl.value = SAMPLE;
inputEl.dataset.llcSample = SAMPLE;
let sampleCleared = false;
inputEl.addEventListener("focus", () => {
if (sampleCleared) return;
if ((inputEl.value || "") === inputEl.dataset.llcSample) {
inputEl.value = "";
sampleCleared = true;
updateOutput_();
}
});
function getViewMode_() {
const checked = wrapper.querySelector('input[name="llc-view"]:checked');
return checked ? checked.value : "chords-over-lyrics";
}
function viewLabel_(mode){
if (mode === "chords-over-lyrics") return "Chord Chart";
if (mode === "numbers-over-lyrics") return "Numbers Chart";
if (mode === "lyrics-only") return "Lyrics";
if (mode === "canonical-numbers") return "Bracketed Numbers";
if (mode === "canonical-chords") return "Bracketed Chords";
return "Chart";
}
function exportTitle_(songTitle, key, mode){
const base = llTitleCase_(songTitle || "Chart") || "Chart";
const label = viewLabel_(mode);
return base + " - " + key + " [" + label + "]";
}
function buildHeaderParts_() {
const titleRaw = (titleInput.value || "").trim();
const writerRaw = (writerInput.value || "").trim();
const tempoRaw = (tempoInput.value || "").trim();
const bpm = String(bpmInput.value || "").trim();
const meter = (meterInput.value || "").trim();
const key = keySel.value;
const mode = getViewMode_();
const title = llTitleCase_(titleRaw) || "";
const writer = llTitleCase_(writerRaw) || "";
const tempo = llTitleCase_(tempoRaw) || "";
const metaBits = [];
metaBits.push({ label: "Key", value: key });
if (tempo) metaBits.push({ label: "Tempo", value: tempo });
if (bpm) metaBits.push({ label: "BPM", value: bpm });
if (meter) metaBits.push({ label: "Meter", value: meter });
return {
title: title || "Chart",
writer: writer || "",
tempo: tempo || "",
bpm: bpm || "",
meter: meter || "",
metaBits,
key,
mode,
viewLabel: viewLabel_(mode),
printTitle: exportTitle_(title, key, mode)
};
}
function updateTitleBox_(parts){
titleBoxEl.style.display = "block";
titleBoxTitleEl.textContent = parts.title;
titleBoxWriterEl.textContent = parts.writer ? parts.writer : "";
titleBoxWriterEl.style.display = parts.writer ? "block" : "none";
titleBoxMetaEl.innerHTML = "";
parts.metaBits.forEach((b, idx) => {
const span = document.createElement("span");
span.textContent = b.label + ": " + b.value;
titleBoxMetaEl.appendChild(span);
if (idx < parts.metaBits.length - 1) {
const dot = document.createElement("span");
dot.className = "llc-dot";
dot.textContent = "|";
titleBoxMetaEl.appendChild(dot);
}
});
}
function updateOutContext_(parts){
outCtxEl.textContent = parts.key + " • " + parts.viewLabel;
}
function llSyncHeights_(){
const hIn = inputEl.offsetHeight;
const hOut = outputEl.offsetHeight;
const target = Math.max(hIn, hOut);
inputEl.style.height = target + "px";
outputEl.style.height = target + "px";
}
function setupLinkedResize_(){
if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver(() => llSyncHeights_());
ro.observe(inputEl);
ro.observe(outputEl);
}
["mouseup","touchend"].forEach(ev => {
inputEl.addEventListener(ev, llSyncHeights_);
outputEl.addEventListener(ev, llSyncHeights_);
});
window.addEventListener("resize", llSyncHeights_);
requestAnimationFrame(llSyncHeights_);
}
function computeChart_(parts, original){
if (parts.mode === "chords-over-lyrics") return llRenderOverLyrics(original, "chords", parts.key);
if (parts.mode === "numbers-over-lyrics") return llRenderOverLyrics(original, "numbers", parts.key);
if (parts.mode === "lyrics-only") return llLyricsOnly_(original);
if (parts.mode === "canonical-numbers") return (original || "").trim();
if (parts.mode === "canonical-chords") return llCanonicalToChordCanonical(original || "", parts.key).trim();
return "";
}
function updateOutput_() {
const original = inputEl.value || "";
const parts = buildHeaderParts_();
updateTitleBox_(parts);
updateOutContext_(parts);
const chart = computeChart_(parts, original);
outputEl.innerHTML = llBuildRichPreHtml_(chart.trimEnd());
outputEl.dataset.chartOnly = chart.trimEnd();
outputEl.dataset.printTitle = parts.printTitle;
statusEl.textContent = "";
llSyncHeights_();
}
[titleInput, writerInput, tempoInput, bpmInput, meterInput, keySel, inputEl].forEach(el => el.addEventListener("input", updateOutput_));
wrapper.querySelectorAll('input[name="llc-view"]').forEach(r => r.addEventListener("change", updateOutput_));
copyBtn.addEventListener("click", async () => {
const parts = buildHeaderParts_();
const chart = (outputEl.dataset.chartOnly || "");
const metaLine = parts.metaBits.map(b => b.label + ": " + b.value).join(" | ");
const text = (parts.title + "\n" + (parts.writer ? parts.writer + "\n" : "") + metaLine + "\n\n" + chart).trimEnd();
if (!text.trim()) return;
try {
await navigator.clipboard.writeText(text);
statusEl.textContent = "Copied ✔";
setTimeout(() => statusEl.textContent = "", 1400);
} catch {
statusEl.textContent = "Copy failed.";
setTimeout(() => statusEl.textContent = "", 1600);
}
});
/* ---------------------------------------------------------
PRINT (Safari reliable)
- One clean header
- Flow layout
- Only break between sections
- Footer line at end
- Add 3 blank lines after last chart
---------------------------------------------------------- */
printBtn.addEventListener("click", () => {
const w = window.open("", "_blank");
if (!w) {
alert("Pop-up blocked. Please allow pop-ups to view/print the chart.");
return;
}
const parts = buildHeaderParts_();
const chartText = String(outputEl.dataset.chartOnly || "").replace(/\r\n/g, "\n");
const lines = chartText.split("\n");
const html = `
${llEscapeHtml_(parts.printTitle)}
${LL_SCRIPT_OPEN}
const lines = ${JSON.stringify(lines)};
function looksLikeChordLine(line){
const t = String(line || "").trim();
if (!t) return false;
const chordToken = /(?:^|\\s)([A-G](?:#|b)?)(?:maj|min|m|sus|dim|aug|add)?\\d*(?:\\([^\\)]*\\))?(?:\\/[A-G](?:#|b)?)?(?=\\s|$)/gi;
let count = 0;
while (chordToken.exec(t)) count++;
return count >= 2;
}
function isSectionHeader(line){
const t = String(line || "").trim();
if (!t) return false;
if (t.includes("[") || t.includes("]")) return false;
if (/^(Song Title|Key|Tempo|BPM|Meter|Songwriter)\\s*:/i.test(t)) return false;
if (looksLikeChordLine(t)) return false;
if (/^(VERSE|CHORUS|BRIDGE|INTRO|OUTRO|TAG|REFRAIN|PRE[- ]?CHORUS|INTERLUDE|INSTRUMENTAL|TURNAROUND)(\\s+\\d+)?(\\s*\\(.*\\))?$/i.test(t)) {
return true;
}
if (/^[A-Z0-9][A-Z0-9 \\-()]{2,}$/.test(t)) {
const words = t.replace(/[()\\-]/g, " ").split(/\\s+/).filter(Boolean);
const hasWordyToken = words.some(w => /^[A-Z]{3,}$/.test(w));
return hasWordyToken;
}
return false;
}
function esc(s){
return String(s).replace(/[&<>"']/g, (m) => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[m]));
}
const sections = [];
let current = { title: "", body: [] };
function pushCurrent(){
const hasInk = (current.title && current.title.trim()) ||
current.body.some(l => String(l||"").trim() !== "");
if (hasInk) sections.push(current);
}
for (let i=0;i {
const wrap = document.createElement("div");
wrap.className = "section";
const titleHtml = sec.title ? '";
const SP_SCRIPT_CLOSE = " ";
const LL_MAJOR_SCALES = ${JSON.stringify(LL_MAJOR_SCALES)};
function llTitleCase_(s){
const txt = String(s || "").trim();
if (!txt) return "";
return txt.replace(/\\s+/g," ").split(" ").map(w=>{
if(!w) return w;
if (/^[A-Z0-9/]+$/.test(w) && w.length<=5) return w;
return w[0].toUpperCase()+w.slice(1);
}).join(" ");
}
function llUnwrapParens_(s){
const t = String(s || "").trim();
const m = t.match(/^\\(\\s*(.+?)\\s*\\)$/);
return m ? { inner:m[1], wrapped:true } : { inner:t, wrapped:false };
}
function llWrapMaybe_(s, wrapped){ return wrapped ? "(" + s + ")" : s; }
function llParseChordToken(token) {
token = (token || "").trim();
if (!token) return null;
const { inner } = llUnwrapParens_(token);
token = inner;
let root = token;
let bass = null;
const slashIndex = token.indexOf("/");
if (slashIndex !== -1) {
root = token.slice(0, slashIndex);
bass = token.slice(slashIndex + 1);
}
function splitPart(part) {
const match = part.match(/^([1-7])(.+)?$/);
if (!match) return null;
return { degree: parseInt(match[1], 10), quality: match[2] || "" };
}
const rootInfo = splitPart(root);
if (!rootInfo) return null;
const bassInfo = bass ? splitPart(bass) : null;
return { root: rootInfo, bass: bassInfo };
}
function llNumberToChord(token, key) {
const scale = LL_MAJOR_SCALES[key];
if (!scale) return token;
const { inner, wrapped } = llUnwrapParens_(token);
const parsed = llParseChordToken(inner);
if (!parsed) return llWrapMaybe_(inner, wrapped);
const rootNote = scale[parsed.root.degree - 1] || "?";
let chord = rootNote + (parsed.root.quality || "");
if (parsed.bass) {
const bassNote = scale[parsed.bass.degree - 1] || "?";
chord += "/" + bassNote;
}
return llWrapMaybe_(chord, wrapped);
}
function llCanonicalToChordCanonical(text, key) {
return text.replace(/\\[([^\\]]+)\\]/g, (match, token) => "[" + llNumberToChord(token.trim(), key) + "]");
}
function llLooksLikeChordLine_(line){
const t = String(line || "").trim();
if (!t) return false;
const chordToken = /(?:^|\\s)([A-G](?:#|b)?)(?:maj|min|m|sus|dim|aug|add)?\\d*(?:\\([^\\)]*\\))?(?:\\/[A-G](?:#|b)?)?(?=\\s|$)/gi;
let count = 0;
while (chordToken.exec(t)) count++;
return count >= 2;
}
function llIsSectionHeaderLine_(line){
const t = (line || "").trim();
if (!t) return false;
if (t.includes("[") || t.includes("]")) return false;
if (/^(Song Title|Key|Tempo|BPM|Meter|Songwriter)\\s*:/i.test(t)) return false;
if (llLooksLikeChordLine_(t)) return false;
if (/^(VERSE|CHORUS|BRIDGE|INTRO|OUTRO|TAG|REFRAIN|PRE[- ]?CHORUS|INTERLUDE|INSTRUMENTAL|TURNAROUND)(\\s+\\d+)?(\\s*\\(.*\\))?$/i.test(t)) {
return true;
}
if (/^[A-Z0-9][A-Z0-9 \\-()]{2,}$/.test(t)) {
const words = t.replace(/[()\\-]/g, " ").split(/\\s+/).filter(Boolean);
const hasWordyToken = words.some(w => /^[A-Z]{3,}$/.test(w));
return hasWordyToken;
}
return false;
}
function llEscapeHtml_(s) {
return String(s).replace(/[&<>"']/g, (m) => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[m]));
}
function llBuildRichPreHtml_(plainText){
const lines = String(plainText || "").replace(/\\r\\n/g, "\\n").split("\\n");
return lines.map(line => {
const esc = llEscapeHtml_(line);
if (llIsSectionHeaderLine_(line)) return ''+esc+'';
return esc;
}).join("\\n");
}
function llRenderOverLyrics(text, mode, key) {
const lines = text.replace(/\\r\\n/g, "\\n").split("\\n");
const outLines = [];
lines.forEach(line => {
const trimmed = line.trim();
if (!line.includes("[")) {
outLines.push(trimmed === "" ? "" : trimmed);
return;
}
let lyricsLine = "";
const chordChars = [];
function placeChordAt(pos, token) {
if (!token) return;
const chordSymbol = (mode === "chords") ? llNumberToChord(token, key) : token;
while (chordChars.length <= pos) chordChars.push(" ");
for (let i = 0; i < chordSymbol.length; i++) chordChars[pos + i] = chordSymbol[i];
}
let i = 0;
while (i < line.length) {
const ch = line[i];
if (ch === "[") {
const end = line.indexOf("]", i + 1);
if (end !== -1) {
const token = line.slice(i + 1, end).trim();
const pos = Math.max(lyricsLine.length, 0);
placeChordAt(pos, token);
i = end + 1;
continue;
}
}
lyricsLine += ch;
while (chordChars.length < lyricsLine.length) chordChars.push(" ");
i++;
}
const chordLine = chordChars.join("").replace(/\\s+$/,"");
if (chordLine.trim()) {
outLines.push(chordLine);
outLines.push(lyricsLine.replace(/\\s+$/,""));
} else {
outLines.push(lyricsLine.replace(/\\s+$/,""));
}
});
return outLines.join("\\n");
}
function llLyricsOnly_(text){
const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
const out = lines.map(line => {
if (!line.includes("[")) return line.replace(/\\s+$/,"");
const stripped = line.replace(/\\[[^\\]]*\\]/g, "");
return stripped.replace(/\\s+$/,"").replace(/[ \\t]{2,}/g, " ");
});
return out.join("\\n").trimEnd();
}
function getSongData(){
const ta = document.getElementById("ll-song-data");
if(!ta) return null;
try{ return JSON.parse(ta.value || "{}"); }catch{ return null; }
}
const data = getSongData() || {};
const mount = document.getElementById("ll-song-page");
if(!mount) return;
const title = llTitleCase_(data.title || "Song") || "Song";
const writer = llTitleCase_(data.writer || "");
const tempo = llTitleCase_(data.tempo || "");
const bpm = String(data.bpm || "").trim();
const meter = String(data.meter || "").trim();
const canonical = String(data.canonical || "");
const defaultKey = data.defaultKey || "E";
mount.innerHTML = \`
\`;
const keySel = document.getElementById("llsp-key");
Object.keys(LL_MAJOR_SCALES).forEach(k=>{
const opt=document.createElement("option");
opt.value=k; opt.textContent=k;
if(k===defaultKey) opt.selected=true;
keySel.appendChild(opt);
});
const metaEl = document.getElementById("llsp-meta");
const ctxEl = document.getElementById("llsp-ctx");
const outEl = document.getElementById("llsp-out");
const stEl = document.getElementById("llsp-status");
function viewLabel_(mode){
if (mode === "chords-over-lyrics") return "Chord Chart";
if (mode === "numbers-over-lyrics") return "Numbers Chart";
if (mode === "lyrics-only") return "Lyrics";
if (mode === "canonical-numbers") return "Bracketed Numbers";
if (mode === "canonical-chords") return "Bracketed Chords";
return "Chart";
}
function buildMetaLine_(){
const key = keySel.value;
const bits = [];
bits.push({label:"Key", value:key});
if(tempo) bits.push({label:"Tempo", value:tempo});
if(bpm) bits.push({label:"BPM", value:bpm});
if(meter) bits.push({label:"Meter", value:meter});
return bits;
}
function updateMeta_(){
metaEl.innerHTML="";
const bits = buildMetaLine_();
bits.forEach((b, idx)=>{
const s=document.createElement("span");
s.textContent = b.label + ": " + b.value;
metaEl.appendChild(s);
if(idx < bits.length-1){
const d=document.createElement("span");
d.className="llsp-dot";
d.textContent="|";
metaEl.appendChild(d);
}
});
}
function getMode_(){
const checked = document.querySelector('input[name="llsp-view"]:checked');
return checked ? checked.value : "chords-over-lyrics";
}
function computeChart_(mode, key){
if (mode === "chords-over-lyrics") return llRenderOverLyrics(canonical, "chords", key);
if (mode === "numbers-over-lyrics") return llRenderOverLyrics(canonical, "numbers", key);
if (mode === "lyrics-only") return llLyricsOnly_(canonical);
if (mode === "canonical-numbers") return canonical.trim();
if (mode === "canonical-chords") return llCanonicalToChordCanonical(canonical, key).trim();
return "";
}
function update_(){
const key = keySel.value;
const mode = getMode_();
updateMeta_();
ctxEl.textContent = key + " • " + viewLabel_(mode);
const chart = computeChart_(mode, key);
outEl.innerHTML = llBuildRichPreHtml_(chart.trimEnd());
outEl.dataset.chartOnly = chart.trimEnd();
outEl.dataset.printTitle = (title || "Song") + " - " + key + " [" + viewLabel_(mode) + "]";
stEl.textContent = "";
}
keySel.addEventListener("input", update_);
document.querySelectorAll('input[name="llsp-view"]').forEach(r=>r.addEventListener("change", update_));
document.getElementById("llsp-copy").addEventListener("click", async ()=>{
const key = keySel.value;
const mode = getMode_();
const bits = buildMetaLine_();
const metaLine = bits.map(b=>b.label + ": " + b.value).join(" | ");
const chart = String(outEl.dataset.chartOnly || "");
const txt = (title + "\\n" + (writer ? writer + "\\n" : "") + metaLine + "\\n\\n" + chart).trimEnd();
try{
await navigator.clipboard.writeText(txt);
stEl.textContent="Copied ✔";
setTimeout(()=>stEl.textContent="", 1400);
}catch{
stEl.textContent="Copy failed.";
setTimeout(()=>stEl.textContent="", 1600);
}
});
document.getElementById("llsp-print").addEventListener("click", ()=>{
const w = window.open("", "_blank");
if(!w){ alert("Pop-up blocked. Please allow pop-ups to view/print the chart."); return; }
const key = keySel.value;
const mode = getMode_();
const bits = buildMetaLine_();
const metaLine = bits.map(b=>b.label + ": " + b.value).join(" | ");
const chartText = String(outEl.dataset.chartOnly || "").replace(/\\r\\n/g,"\\n");
const lines = chartText.split("\\n");
const printTitle = (title || "Song") + " - " + key + " [" + viewLabel_(mode) + "]";
const escTitle = (s)=>String(s).replace(/[&<>"']/g, m=>({ "&":"&","<":"<",">":">",'"':""","'":"'" }[m]));
const html = \`
\${escTitle(printTitle)}
\${SP_SCRIPT_OPEN}
const lines = \${JSON.stringify(lines)};
function looksLikeChordLine(line){
const t = String(line || "").trim();
if(!t) return false;
const chordToken = /(?:^|\\s)([A-G](?:#|b)?)(?:maj|min|m|sus|dim|aug|add)?\\d*(?:\\([^\\)]*\\))?(?:\\/[A-G](?:#|b)?)?(?=\\s|$)/gi;
let count=0; while(chordToken.exec(t)) count++;
return count>=2;
}
function isSectionHeader(line){
const t = String(line||"").trim();
if(!t) return false;
if(t.includes("[") || t.includes("]")) return false;
if(/^(Song Title|Key|Tempo|BPM|Meter|Songwriter)\\s*:/i.test(t)) return false;
if(looksLikeChordLine(t)) return false;
if(/^(VERSE|CHORUS|BRIDGE|INTRO|OUTRO|TAG|REFRAIN|PRE[- ]?CHORUS|INTERLUDE|INSTRUMENTAL|TURNAROUND)(\\s+\\d+)?(\\s*\\(.*\\))?$/i.test(t)) return true;
if(/^[A-Z0-9][A-Z0-9 \\-()]{2,}$/.test(t)){
const words = t.replace(/[()\\-]/g," ").split(/\\s+/).filter(Boolean);
return words.some(w=>/^[A-Z]{3,}$/.test(w));
}
return false;
}
function esc(s){
return String(s).replace(/[&<>"']/g, m=>({ "&":"&","<":"<",">":">",'"':""","'":"'" }[m]));
}
const sections=[];
let current={ title:"", body:[] };
function pushCurrent(){
const hasInk = (current.title && current.title.trim()) || current.body.some(l=>String(l||"").trim()!=="");
if(hasInk) sections.push(current);
}
for(let i=0;i{
const wrap=document.createElement("div");
wrap.className="section";
const titleHtml = sec.title ? '
Numbers Chord Chart Converter
About + Formatting Help
Put each chord token in brackets: [1], [2m7], [5sus], [5/7].
Passing/suggested chords can be wrapped like [(4)] or [(5/7)] and will carry through.
Section headers are great too (VERSE, CHORUS, BRIDGE).
Song Details
Add a few details so your output can be copied into Planning Center, a chord chart doc, or a rehearsal note without extra editing.
Key and View
Key:
Bracketed Numbers
Output
${llEscapeHtml_(parts.title)}
${parts.writer ? `${llEscapeHtml_(parts.writer)}
` : ``}
' + esc(sec.title) + '
' : '';
const extra = (idx === sections.length - 1) ? "\\n\\n\\n" : "";
wrap.innerHTML =
titleHtml +
'' + esc((sec.body || []).join("\\n") + extra) + '';
content.appendChild(wrap);
});
requestAnimationFrame(() => window.print());
${LL_SCRIPT_CLOSE}
`;
w.document.open();
w.document.write(html);
w.document.close();
});
/* =========================================================
SONG PAGE EMBED GENERATOR (termination-safe)
Generates a standalone Song Page block derived from inputs.
========================================================= */
function buildSongPageEmbed_(){
const parts = buildHeaderParts_();
const canonical = String(inputEl.value || "");
const songData = {
title: parts.title || "Song",
writer: parts.writer || "",
tempo: parts.tempo || "",
bpm: parts.bpm || "",
meter: parts.meter || "",
defaultKey: parts.key || "E",
defaultView: "chords-over-lyrics",
canonical
};
let safeJson = JSON.stringify(songData, null, 2);
safeJson = safeJson.replace(LL_CLOSE_SCRIPT_RE, LL_CLOSE_SCRIPT_SAFE);
const songPageCss = `
`.trim();
const songPageJs = `
(function(){
const SP_SCRIPT_OPEN = "\${title}
Choose a key and view option below. Your chart is generated from bracketed numbers and formatted for rehearsal and printing.
Key:
\${title}
\${writer ? ''+writer+'
' : ''}
Output
\${escTitle(title)}
\${writer ? ''+escTitle(writer)+'
' : ''}
'+esc(sec.title)+'
' : '';
const extra = (idx===sections.length-1) ? "\\n\\n\\n" : "";
wrap.innerHTML = titleHtml + ''+esc((sec.body||[]).join("\\n")+extra)+'';
content.appendChild(wrap);
});
requestAnimationFrame(()=>window.print());
\${SP_SCRIPT_CLOSE}
\`;
w.document.open(); w.document.write(html); w.document.close();
});
update_();
})();`.trim();
const embed = `
${songPageCss}
${LL_SCRIPT_OPEN}
${songPageJs}
${LL_SCRIPT_CLOSE}
`.trim();
// Final safety pass (and still no " {
const code = buildSongPageEmbed_();
embedTa.value = code;
embedBox.style.display = "block";
embedStatus.textContent = "";
try{
await navigator.clipboard.writeText(code);
embedStatus.textContent = "Embed code generated + copied ✔";
setTimeout(()=>embedStatus.textContent="", 1800);
}catch{
embedStatus.textContent = "Generated. Click “Copy Embed Code”.";
setTimeout(()=>embedStatus.textContent="", 2200);
}
});
embedCopy.addEventListener("click", async () => {
const code = embedTa.value || "";
if (!code.trim()) return;
try{
await navigator.clipboard.writeText(code);
embedStatus.textContent = "Copied ✔";
setTimeout(()=>embedStatus.textContent="", 1600);
}catch{
embedStatus.textContent = "Copy failed.";
setTimeout(()=>embedStatus.textContent="", 1800);
}
});
embedHide.addEventListener("click", () => {
embedBox.style.display = "none";
embedStatus.textContent = "";
});
setupLinkedResize_();
updateOutput_();
}
/* =========================================================
Squarespace-safe boot (Ajax navigation + late injection)
========================================================= */
function llBoot_(){
try{ llInitCodeBuilderApp(); }catch(e){ console.error("LL Code Builder init failed:", e); }
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", llBoot_);
} else {
llBoot_();
}
window.addEventListener("load", llBoot_);
document.addEventListener("mercury:load", llBoot_);
document.addEventListener("mercury:done", llBoot_);
const mo = new MutationObserver(() => llBoot_());
mo.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(llBoot_, 0);
setTimeout(llBoot_, 400);
})();