|
1552 | 1552 | const newKey = newEntries[0]?.key || editingBibKey; |
1553 | 1553 | const singleBib = newBib.trim() + '\n'; |
1554 | 1554 | if (newKey === editingBibKey) { |
1555 | | - // Same key — update in place; fetch fresh SHA from branch |
1556 | | - const oldPath = `${cat.dir}/${editingBibKey}.bib`; |
| 1555 | + // Same key — update in place; use stored path or derive safe filename |
| 1556 | + const oldFileObj = s.files.find(f => (f.entry?.key || f.name.replace(/\.bib$/, '')) === editingBibKey); |
| 1557 | + const oldPath = oldFileObj ? oldFileObj.path : `${cat.dir}/${safeKey(editingBibKey)}.bib`; |
1557 | 1558 | const shaRes = await fetch(`${GH}/contents/${oldPath}?ref=${encodeURIComponent(branch)}`, { headers: ghH() }); |
1558 | 1559 | if (!shaRes.ok) await parseGhError(shaRes, 'Failed to read file SHA from branch'); |
1559 | 1560 | const oldSha = (await shaRes.json()).sha; |
1560 | 1561 | const upRes = await fetch(`${GH}/contents/${oldPath}`, { method: 'PUT', headers: ghH(), |
1561 | 1562 | body: JSON.stringify({ message: `Edit ${editingBibKey} in ${cat.dir}`, content: b64e(singleBib), sha: oldSha, branch }) }); |
1562 | 1563 | if (!upRes.ok) await parseGhError(upRes, 'Failed to update file'); |
1563 | 1564 | } else { |
1564 | | - // Key changed — create new file, delete old file |
1565 | | - const newPath = `${cat.dir}/${newKey}.bib`; |
| 1565 | + // Key changed — create new file with safe unique name, delete old file |
| 1566 | + const oldFileObj2 = s.files.find(f => (f.entry?.key || f.name.replace(/\.bib$/, '')) === editingBibKey); |
| 1567 | + const oldPath = oldFileObj2 ? oldFileObj2.path : `${cat.dir}/${safeKey(editingBibKey)}.bib`; |
| 1568 | + const existingNamesForRename = new Set(s.files.filter(f => f.path !== oldPath).map(f => f.name.replace(/\.bib$/, ''))); |
| 1569 | + let newBaseName = safeKey(newKey), newName = newBaseName, newSuffix = 2; |
| 1570 | + while (existingNamesForRename.has(newName)) { newName = `${newBaseName}_${newSuffix++}`; } |
| 1571 | + const newPath = `${cat.dir}/${newName}.bib`; |
1566 | 1572 | const crRes = await fetch(`${GH}/contents/${newPath}`, { method: 'PUT', headers: ghH(), |
1567 | | - body: JSON.stringify({ message: `Rename ${editingBibKey} → ${newKey} in ${cat.dir}`, content: b64e(singleBib), branch }) }); |
| 1573 | + body: JSON.stringify({ message: `Rename ${editingBibKey} -> ${newKey} in ${cat.dir}`, content: b64e(singleBib), branch }) }); |
1568 | 1574 | if (!crRes.ok) await parseGhError(crRes, 'Failed to create new file'); |
1569 | | - const oldPath = `${cat.dir}/${editingBibKey}.bib`; |
1570 | 1575 | const shaRes = await fetch(`${GH}/contents/${oldPath}?ref=${encodeURIComponent(branch)}`, { headers: ghH() }); |
1571 | 1576 | if (!shaRes.ok) await parseGhError(shaRes, 'Failed to read old file SHA'); |
1572 | 1577 | const oldSha = (await shaRes.json()).sha; |
|
1575 | 1580 | if (!delRes.ok) await parseGhError(delRes, 'Failed to delete old file'); |
1576 | 1581 | } |
1577 | 1582 | } else { |
1578 | | - // New entries — create one file per entry |
| 1583 | + // New entries — create one file per entry, using safe unique filenames |
| 1584 | + const existingNames = new Set(s.files.map(f => f.name.replace(/\.bib$/, ''))); |
| 1585 | + const usedNames = new Set(existingNames); |
1579 | 1586 | for (const entry of newEntries) { |
1580 | | - const filePath = `${cat.dir}/${entry.key}.bib`; |
| 1587 | + let baseName = safeKey(entry.key), name = baseName, suffix = 2; |
| 1588 | + while (usedNames.has(name)) { name = `${baseName}_${suffix++}`; } |
| 1589 | + usedNames.add(name); |
| 1590 | + const filePath = `${cat.dir}/${name}.bib`; |
1581 | 1591 | const singleBib = (extractRawBibEntry(newBib, entry.key) || newBib).trim() + '\n'; |
1582 | 1592 | const crRes = await fetch(`${GH}/contents/${filePath}`, { method: 'PUT', headers: ghH(), |
1583 | 1593 | body: JSON.stringify({ message: `Add ${entry.key} to ${cat.dir}`, content: b64e(singleBib), branch }) }); |
|
1799 | 1809 | body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: baseSha }) }); |
1800 | 1810 | if (!branchRes.ok) await parseGhError(branchRes, 'Failed to create branch'); |
1801 | 1811 | // Fetch fresh SHA of the file from the new branch |
1802 | | - const filePath = fileObj ? fileObj.path : `${cat.dir}/${key}.bib`; |
| 1812 | + const filePath = fileObj ? fileObj.path : `${cat.dir}/${safeKey(key)}.bib`; |
1803 | 1813 | const shaRes = await fetch(`${GH}/contents/${filePath}?ref=${encodeURIComponent(branch)}`, { headers: ghH() }); |
1804 | 1814 | if (!shaRes.ok) await parseGhError(shaRes, 'Failed to read file SHA from branch'); |
1805 | 1815 | const fileSha = (await shaRes.json()).sha; |
|
1908 | 1918 | validateInput(); |
1909 | 1919 | } |
1910 | 1920 |
|
| 1921 | +function safeKey(key) { return key.replace(/[^\w\-]/g, '_'); } |
| 1922 | + |
1911 | 1923 | function parseBib(raw) { |
1912 | 1924 | const out = []; |
1913 | 1925 | const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,([^@]*)/gs; |
1914 | 1926 | let m; |
1915 | | - while ((m = re.exec(raw)) !== null) |
| 1927 | + while ((m = re.exec(raw)) !== null) { |
| 1928 | + // Validate brace balance: body should have exactly one more } than { |
| 1929 | + let depth = 0; |
| 1930 | + for (const c of m[3]) { if (c === '{') depth++; else if (c === '}') depth--; } |
| 1931 | + if (depth !== -1) continue; // unbalanced — missing closing brace, skip |
1916 | 1932 | out.push({ type: m[1].toLowerCase(), key: m[2].trim(), fields: parseFields(m[3]) }); |
| 1933 | + } |
1917 | 1934 | return out; |
1918 | 1935 | } |
1919 | 1936 | function parseFields(body) { |
|
0 commit comments