diff --git a/cypress/e2e/link_creation.cy.js b/cypress/e2e/link_creation.cy.js
index 35b04e7..821bf79 100644
--- a/cypress/e2e/link_creation.cy.js
+++ b/cypress/e2e/link_creation.cy.js
@@ -130,6 +130,8 @@ context('Link Creation', () => {
},
{name: 'unicode dashes in verse ranges', texts: ['Gen 1:1–3', 'Gen 1:4—5'], containerSelector: '.unicode-dash-test'},
{name: 'mixed separators list', texts: ['Rev 1:1', '2-3', '4:5'], containerSelector: '.mixed-separators-test'},
+ {name: 'and separator list', texts: ['I Corinthians 5:11', '6:9-11', '6:18-20', '7:1-3', '7:8-9'], containerSelector: '.and-separator-test'},
+ {name: 'Jude list with and and comma separators', texts: ['Jude 6', '8', '10'], containerSelector: '.jude-list-test'},
];
for (const testCase of listReferenceCases) {
@@ -205,6 +207,64 @@ context('Link Creation', () => {
});
+ it('Should carry chapter context for and-separated list segments', () => {
+ assertLinkAttributes({
+ text: '7:8-9',
+ containerSelector: '.and-separator-test',
+ book: '1CO',
+ reference: '7:8-7:9',
+ hrefIncludes: ['/1CO.7?passageId=1CO.7.8-1CO.7.9'],
+ });
+ });
+
+ it('Should preserve and delimiters between transformed links', () => {
+ cy.get('.and-separator-test')
+ .invoke('text')
+ .then(text => {
+ expect(text.replace(/\s+/g, ' ').trim()).to.equal('I Corinthians 5:11, 6:9-11, 6:18-20, 7:1-3 and 7:8-9');
+ });
+
+ cy.get('.and-separator-test')
+ .find(CONTAINER_SELECTOR)
+ .should('have.length', 5);
+ });
+
+ it('Should parse Jude list segments as chapter 1 references', () => {
+ assertLinkAttributes({
+ text: 'Jude 6',
+ containerSelector: '.jude-list-test',
+ book: 'JUD',
+ reference: '1:6-1:6',
+ hrefIncludes: ['/JUD.1?passageId=JUD.1.6'],
+ });
+ assertLinkAttributes({
+ text: '8',
+ containerSelector: '.jude-list-test',
+ book: 'JUD',
+ reference: '1:8-1:8',
+ hrefIncludes: ['/JUD.1?passageId=JUD.1.8'],
+ });
+ assertLinkAttributes({
+ text: '10',
+ containerSelector: '.jude-list-test',
+ book: 'JUD',
+ reference: '1:10-1:10',
+ hrefIncludes: ['/JUD.1?passageId=JUD.1.10'],
+ });
+ });
+
+ it('Should preserve and delimiter in transformed Jude list text', () => {
+ cy.get('.jude-list-test')
+ .invoke('text')
+ .then(text => {
+ expect(text.replace(/\s+/g, ' ').trim()).to.equal('Jude 6 and 8, 10');
+ });
+
+ cy.get('.jude-list-test')
+ .find(CONTAINER_SELECTOR)
+ .should('have.length', 3);
+ });
+
it('Should preserve original delimiters between transformed links', () => {
cy.get('.mixed-separators-test')
.invoke('text')
diff --git a/cypress/test.html b/cypress/test.html
index 2854cca..323b8f6 100644
--- a/cypress/test.html
+++ b/cypress/test.html
@@ -17,6 +17,7 @@
@@ -35,6 +36,7 @@
Gen 1:1–3 and Gen 1:4—5
jOhN 3:16
Rev 1:1; 2-3, 4:5
+
I Corinthians 5:11, 6:9-11, 6:18-20, 7:1-3 and 7:8-9
Sirach 2:1
Johnson 4:24 should not be transformed into a bible link
safe hover target
diff --git a/js/biblePreviewer.js b/js/biblePreviewer.js
index e53f5ae..b1bf42d 100644
--- a/js/biblePreviewer.js
+++ b/js/biblePreviewer.js
@@ -65,6 +65,7 @@ const books_start_with_number = `(?:${SAMUEL_REG}|${KINGS_REG}|${CHRON_REG}|${MA
const firstPrefix = String.raw`(?:1(?:st)?|I|First)\s*`;
const secondPrefix = String.raw`(?:2(?:nd)?|II|Second)\s*`;
const thirdPrefix = String.raw`(?:3(?:rd)?|III|Third)\s*`;
+const VERSE_LIST_CONNECTOR_REG = String.raw`(?:[,:;${DASHES_STR}]|\s+and\b)`;
const JUDE_BOOK_ID = 'Jud';
@@ -150,22 +151,26 @@ const bibleBooks = {
'R(?:e?v|evelation)': 'Rev'
};
-let bibleRegex;
-
-if (GENERATE_REGEX) {
+/**
+ * Build the bible reference regex used to detect references in page text.
+ * @returns {RegExp} A case-insensitive, global bible reference matcher
+ */
+function buildBibleRegex() {
// The regex to match book names
- bibleRegex = `(${Object.keys(bibleBooks).join('|')})`;
- // Matches a required start chapter and verse, then matches an optional end chapter and verse, with lists also supported
- bibleRegex += `\\.?\\s*(\\d{1,3}:\\s*\\d{1,3}(?:[,;:${DASHES_STR}]\\s*\\d{1,3}`;
+ let generatedRegex = `(${Object.keys(bibleBooks).join('|')})`;
+ // Matches a required start chapter and verse, then optional continuation delimiters and digits
+ generatedRegex += `\\.?\\s*(\\d{1,3}:\\s*\\d{1,3}(?:${VERSE_LIST_CONNECTOR_REG}\\s*\\d{1,3}`;
// But don't match a single verse if it is right before a book that has a number before it
- bibleRegex += `(?!\\s*${books_start_with_number}))*)`;
+ generatedRegex += `(?!\\s*${books_start_with_number}))*)`;
// Add Jude separately because Jude only has 1 chapter, so people usually don't put a chapter with the verse
- bibleRegex += `|${JUDE_REG}\\s*(\\d{1,2}(?:[,;]?\\s*\\d{1,2})*)`;
- bibleRegex = new RegExp(bibleRegex, 'gi');
+ generatedRegex += `|${JUDE_REG}\\s*(\\d{1,2}(?:(?:[;,]|\\s+and\\b)?\\s*\\d{1,2})*)`;
+ return new RegExp(generatedRegex, 'gi');
+}
+
+const bibleRegex = buildBibleRegex();
+
+if (GENERATE_REGEX) {
console.log(bibleRegex);
-} else {
- // eslint-disable-next-line max-len
- bibleRegex = /(Ge?n(?:esis)?|Ex(?:od(?:us)?)?|Le(?:v(?:iticus)?)?|Nu?m(?:b(?:ers)?)?|D(?:t|eut(?:eronomy)?)|Jo(?:s(?:h(?:ua)?)?)?|J(?:dgs?|udg(?:es)?)|Ru?th|(?:1(?:st)?|I|First)\s*Sa?m(?:uel)?|(?:2(?:nd)?|II|Second)\s*Sa?m(?:uel)?|(?:1(?:st)?|I|First)\s*K(?:in)?gs|(?:2(?:nd)?|II|Second)\s*K(?:in)?gs|(?:1(?:st)?|I|First)\s*Chr(?:on(?:icles)?)?|(?:2(?:nd)?|II|Second)\s*Chr(?:on(?:icles)?)?|Ezra?|Ne(?:h(?:emiah)?)?|Tob(?:it|ias)?|J(?:d?th?|udith)|Est(?:h(?:er)?)?|(?:1(?:st)?|I|First)\s*Mac(?:c(?:abees)?)?|(?:2(?:nd)?|II|Second)\s*Mac(?:c(?:abees)?)?|Jo?b|Ps(?:a(?:lms?)?)?|Pro(?:v(?:erbs)?)?|Ecc(?:les?|lesiastes)?|So(?:S|ng(?:\s*of\s*(?:Sol(?:omon)?|Songs?))?)|Wis(?:dom)?(?:\s*of\s*Sol(?:omon)?)?|Sir(?:ach)?|Bar(?:uch)?|Is(?:a(?:iah)?)?|Jer(?:emiah)?|Lam(?:entations)?|Ez(?:e?k?|ekiel)|Da?n(?:iel)?|Hos(?:ea)?|Joel|Amos|Ob(?:ad(?:iah)?)?|Jon(?:ah)?|Mic(?:ah)?|Nah(?:um)?|Hab(?:akkuk)?|Zep(?:h(?:aniah)?)?|Hag(?:gai)?|Zec(?:h(?:ariah)?)?|Mal(?:achi)?|M(?:t|att(?:h(?:ew)?)?)|M(?:k|ark?)|L(?:k|uke?)|Jo?h?n|Acts?|Ro(?:m(?:ans)?)?|(?:1(?:st)?|I|First)\s*Co(?:r(?:inthians?)?)?|(?:2(?:nd)?|II|Second)\s*Co(?:r(?:inthians?)?)?|Gal(?:atians)?|Eph(?:es(?:ians)?)?|Phil(?:ippians)?|Col(?:ossians)?|(?:1(?:st)?|I|First)\s*Thes(?:s(?:alonians)?)?|(?:2(?:nd)?|II|Second)\s*Thes(?:s(?:alonians)?)?|(?:1(?:st)?|I|First)\s*T(?:imothy|im|i|m)|(?:2(?:nd)?|II|Second)\s*T(?:imothy|im|i|m)|Titus|Phil(?:em(?:on)?)?|Heb(?:rews?)?|Ja(?:me)?s|(?:1(?:st)?|I|First)\s*Pe?t(?:er)?|(?:2(?:nd)?|II|Second)\s*Pe?t(?:er)?|(?:1(?:st)?|I|First)\s*Jo?h?n|(?:2(?:nd)?|II|Second)\s*Jo?h?n|(?:3(?:rd)?|III|Third)\s*Jo?h?n|Jude?|R(?:e?v|evelation))\.?\s*(\d{1,3}:\s*\d{1,3}(?:[,;:–—-]\s*\d{1,3}(?!\s*(?:Sa?m(?:uel)?|K(?:in)?gs|Chr(?:on(?:icles)?)?|Mac(?:c(?:abees)?)?|Co(?:r(?:inthians?)?)?|Thes(?:s(?:alonians)?)?|T(?:imothy|im|i|m)|Pe?t(?:er)?|Jo?h?n)))*)|Jude?\s*(\d{1,2}(?:[,;]?\s*\d{1,2})*)/gi;
}
/**
@@ -244,8 +249,7 @@ function transformBibleReferences(element, trans, language) {
let startChap, startVerse, endChap, endVerse, previousChap = '';
let referenceList = [];
const {verses: verseList} = splitVerseListString(verseListString);
- const splitText = orig.split(/[,;]/g);
- const delimiters = orig.match(/[;,]\s*/g) ?? [];
+ const {verses: splitText, delimiters} = splitVerseListString(orig);
for (const [index, element] of verseList.entries()) {
[startChap, startVerse, endChap, endVerse, previousChap] = getVerseFromString(element, previousChap);
book = book.toUpperCase();
diff --git a/js/verseParser.mjs b/js/verseParser.mjs
index 40db527..0802189 100644
--- a/js/verseParser.mjs
+++ b/js/verseParser.mjs
@@ -1,5 +1,6 @@
const DASHES_STR = '–—-';
const DASHES_REG = new RegExp(`[${DASHES_STR}]`);
+const VERSE_LIST_DELIMITER_REG = /(?:[;,]\s*|\s+and\s+)/gi;
/**
* Given a string, gets the verse components and previous chapter (if it exists)
@@ -41,8 +42,11 @@ function getVerseFromString(verseString, previousChap) {
* @returns {{verses: string[], delimiters: string[]}} Parsed verses and delimiters (in order)
*/
function splitVerseListString(verseListString) {
- const delimiters = verseListString.match(/[;,]\s*/g) ?? [];
- const verses = verseListString.split(/[;,]\s*/g).map(verse => verse.trim());
+ const delimiters = verseListString.match(VERSE_LIST_DELIMITER_REG) ?? [];
+ const verses = verseListString
+ .split(VERSE_LIST_DELIMITER_REG)
+ .map(verse => verse.trim())
+ .filter(Boolean);
return {verses, delimiters};
}
diff --git a/js/verseParser.test.mjs b/js/verseParser.test.mjs
index 10b77f3..3a1fa16 100644
--- a/js/verseParser.test.mjs
+++ b/js/verseParser.test.mjs
@@ -71,3 +71,29 @@ test('splits verse lists and preserves mixed delimiters', () => {
delimiters: [';', ', ']
});
});
+
+
+test('splits verse lists and preserves and delimiters', () => {
+ assert.deepEqual(splitVerseListString('5:11, 6:9-11 and 6:18-20'), {
+ verses: ['5:11', '6:9-11', '6:18-20'],
+ delimiters: [', ', ' and ']
+ });
+});
+
+test('splits verse lists and preserves mixed punctuation and and delimiters', () => {
+ assert.deepEqual(splitVerseListString('7:1-3 AND 7:8-9; 7:10'), {
+ verses: ['7:1-3', '7:8-9', '7:10'],
+ delimiters: [' AND ', '; ']
+ });
+});
+
+test('splits Jude-style numeric lists and preserves and delimiter', () => {
+ assert.deepEqual(splitVerseListString('6 and 8, 10'), {
+ verses: ['6', '8', '10'],
+ delimiters: [' and ', ', ']
+ });
+});
+
+test('parses Jude chapter-one mapped range correctly', () => {
+ assert.deepEqual(getVerseFromString('1:6-8', ''), ['1', '6', '1', '8', '1']);
+});
diff --git a/package-lock.json b/package-lock.json
index 590c67e..05d65e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12320,16 +12320,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.16",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
- "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
+ "version": "5.3.17",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz",
+ "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
@@ -21766,15 +21765,14 @@
}
},
"terser-webpack-plugin": {
- "version": "5.3.16",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
- "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
+ "version": "5.3.17",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz",
+ "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
}
},
diff --git a/package.json b/package.json
index 39c2c65..c316756 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"zip": "rm -rf *.zip && bash scripts/get_versions.sh && python scripts/create_zips.py",
"watch": "webpack --watch",
"cypress:gui": "cypress open",
- "cypress:run": "cypress run --record false --browser chrome --headed",
+ "cypress:run": "cypress run --record false --browser C:\\Users\\codyg\\Documents\\CodingProjects\\BiblePreviewer\\chrome\\win64-146.0.7680.31\\chrome-win64\\chrome.exe --headed",
"test": "node --test",
"test:ci": "npm run test && npm run cypress:run",
"lint": "eslint . --max-warnings 0 --ignore-pattern dist && stylelint **/*.scss",