diff --git a/src/1-one/change/api/replace.js b/src/1-one/change/api/replace.js index b31dec1..84d60cd 100644 --- a/src/1-one/change/api/replace.js +++ b/src/1-one/change/api/replace.js @@ -1,3 +1,5 @@ +import fmts from '../../output/api/_fmts.js' + const dollarStub = /\$[0-9a-z]+/g const fns = {} @@ -6,10 +8,30 @@ const isTitleCase = (str) => /^\p{Lu}[\p{Ll}'’]/u.test(str) || /^\p{Lu}$/u.tes const toTitleCase = (str) => str.replace(/^\p{Ll}/u, x => x.toUpperCase()) const toLowerCase = (str) => str.replace(/^\p{Lu}/u, x => x.toLowerCase()) +// the match's pre and post are kept by replaceWith(), so m.text() should leave them out +const withoutEnds = function (m) { + const text = m.text + m.text = fmt => { + const opts = typeof fmt === 'string' ? fmts[fmt] : fmt + return text.call(m, { keepEndPunct: false, ...opts }) + } + return m +} + +// put the match's own punctuation around the replacement's, unless it already has it +const outerPre = (pre, fresh) => { + const punct = pre.trimStart() + return fresh.startsWith(punct) ? pre.slice(0, pre.length - punct.length) + fresh : pre + fresh +} +const outerPost = (post, fresh) => { + const punct = post.trimEnd() + return fresh.endsWith(punct) ? fresh + post.slice(punct.length) : fresh + post +} + // doc.replace('foo', (m)=>{}) const replaceByFn = function (main, fn, keep) { main.forEach(m => { - const out = fn(m) + const out = fn(withoutEnds(m)) m.replaceWith(out, keep) }) return main @@ -58,13 +80,23 @@ fns.replaceWith = function (input, keep = {}) { ptrs = ptrs.map(ptr => ptr.slice(0, 3)) // original.freeze() let oldTags = (original.docs[0] || []).map(term => Array.from(term.tags)) - const originalPre = original.docs[0][0].pre - const originalPost = original.docs[0].at(-1).post + const originalEnds = original.docs.map(words => [words[0].pre, words.at(-1).post]) // slide this in if (typeof input === 'string') { input = this.fromText(input).compute('id') } + const fresh = input.docs[0] main.insertAfter(input) + // keep the punctuation around each match, outside the replacement's own + if (fresh) { + main.docs.forEach((words, i) => { + const [pre, post] = originalEnds[i] + words.at(-fresh.length).pre = outerPre(pre, fresh[0].pre.trimStart()) + if (post) { + words.at(-1).post = outerPost(post, fresh.at(-1).post.trimEnd()) + } + }) + } // are we replacing part of a contraction? if (original.has('@hasContraction') && main.contractions) { const more = main.grow('@hasContraction+') @@ -84,17 +116,6 @@ fns.replaceWith = function (input, keep = {}) { } } - // try to keep some pre-punctuation - if (originalPre && main.docs[0]) { - main.docs[0][0].pre = originalPre - } - // try to keep any post-punctuation - if (originalPost && main.docs[0]) { - const lastOne = main.docs[0].at(-1) - if (!lastOne.post.trim()) { - lastOne.post = originalPost - } - } // what should we return? const m = main.toView(ptrs).compute(['index', 'freeze', 'lexicon']) if (m.world.compute.preTagger) { diff --git a/tests/02-two/change/replace.test.js b/tests/02-two/change/replace.test.js index 434d9df..762ff06 100644 --- a/tests/02-two/change/replace.test.js +++ b/tests/02-two/change/replace.test.js @@ -225,3 +225,43 @@ test('replace-with-pre-post', function (t) { t.end() }) + +test('replace-edge-punctuation', function (t) { + let arr = [ + ['"hello world" he said', '"[x]" he said'], + ['he said "hello world" today', 'he said "[x]" today'], + ['he said "hello world"', 'he said "[x]"'], + ['(hello world) he said', '([x]) he said'], + ['he said (hello world) today', 'he said ([x]) today'], + ['he said hello world.', 'he said [x].'], + ['"hello world" and "hello world" too', '"[x]" and "[x]" too'], + ] + arr.forEach(a => { + const doc = nlp(a[0]) + doc.replace('hello world', '[x]') + t.equal(doc.text(), a[1], here + 'string: ' + a[0]) + }) + + arr = [ + ['"hello world" he said', '"HELLO WORLD" he said'], + ['he said "hello world" today', 'he said "HELLO WORLD" today'], + ['he said "hello world"', 'he said "HELLO WORLD"'], + ['(hello world) he said', '(HELLO WORLD) he said'], + ['hello world.', 'HELLO WORLD.'], + ['"hello world" and "hello world" too', '"HELLO WORLD" and "HELLO WORLD" too'], + ] + arr.forEach(a => { + const doc = nlp(a[0]) + doc.replace('hello world', m => m.text().toUpperCase()) + t.equal(doc.text(), a[1], here + 'function: ' + a[0]) + }) + + let doc = nlp('"hello world" he said') + doc.replace('hello world', m => '[' + m.text() + ']') + t.equal(doc.text(), '"[hello world]" he said', here + 'function with brackets') + + doc = nlp('he said "hello" today') + doc.replace('hello', '"hi"') + t.equal(doc.text(), 'he said "hi" today', here + 'quotes already in replacement') + t.end() +})