From b3dcee76174d91045171a8b1382ed3330545c01c Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 14 Oct 2026 19:51:12 +0200 Subject: [PATCH] Upstream spencermountain/compromise state of the touched files before 33ab46246d --- src/1-one/change/api/replace.js | 134 ++++++++++++++++ tests/02-two/change/replace.test.js | 227 ++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 src/1-one/change/api/replace.js create mode 100644 tests/02-two/change/replace.test.js diff --git a/src/1-one/change/api/replace.js b/src/1-one/change/api/replace.js new file mode 100644 index 0000000..b31dec1 --- /dev/null +++ b/src/1-one/change/api/replace.js @@ -0,0 +1,134 @@ +const dollarStub = /\$[0-9a-z]+/g +const fns = {} + +// case logic +const isTitleCase = (str) => /^\p{Lu}[\p{Ll}'’]/u.test(str) || /^\p{Lu}$/u.test(str) +const toTitleCase = (str) => str.replace(/^\p{Ll}/u, x => x.toUpperCase()) +const toLowerCase = (str) => str.replace(/^\p{Lu}/u, x => x.toLowerCase()) + +// doc.replace('foo', (m)=>{}) +const replaceByFn = function (main, fn, keep) { + main.forEach(m => { + const out = fn(m) + m.replaceWith(out, keep) + }) + return main +} + +// support 'foo $0' replacements +const subDollarSign = function (input, main) { + if (typeof input !== 'string') { + return input + } + const groups = main.groups() + input = input.replace(dollarStub, a => { + const num = a.replace(/\$/, '') + if (Object.hasOwn(groups, num)) { + return groups[num].text() + } + return a + }) + return input +} + +fns.replaceWith = function (input, keep = {}) { + let ptrs = this.fullPointer + // support keep-all option + if (keep === true) { + keep = { + tags: true, + case: true, + possessives: true, + } + } + const main = this + this.uncache() + if (typeof input === 'function') { + return replaceByFn(main, input, keep) + } + const terms = main.docs[0] + if (!terms) return main + const isOriginalPossessive = keep.possessives && terms.at(-1).tags.has('Possessive') + const isOriginalTitleCase = keep.case && isTitleCase(terms[0].text) + // support 'foo $0' replacements + input = subDollarSign(input, main) + + const original = this.update(ptrs) + // soften-up pointer + 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 + // slide this in + if (typeof input === 'string') { + input = this.fromText(input).compute('id') + } + main.insertAfter(input) + // are we replacing part of a contraction? + if (original.has('@hasContraction') && main.contractions) { + const more = main.grow('@hasContraction+') + more.contractions().expand() + } + // delete the original terms + main.delete(original) //science. + + // keep "John's" + if (isOriginalPossessive) { + const tmp = main.docs[0] + const term = tmp.at(-1) + if (!term.tags.has('Possessive')) { + term.text += "'s" + term.normal += "'s" + term.tags.add('Possessive') + } + } + + // 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) { + m.compute('preTagger') + } + m.compute('unfreeze') + // replace any old tags + if (keep.tags) { + // truncate old tags to only touch new terms + oldTags = oldTags.slice(0, input.wordCount()) + m.terms().forEach((term, i) => { + term.tagSafe(oldTags[i]) + }) + } + + if (!m.docs[0] || !m.docs[0][0]) return m + + // try to co-erce case, too + if (keep.case) { + const transformCase = isOriginalTitleCase ? toTitleCase : toLowerCase + m.docs[0][0].text = transformCase(m.docs[0][0].text) + } + return m +} + +fns.replace = function (match, input, keep) { + if (match && !input) { + return this.replaceWith(match, keep) + } + const m = this.match(match) + if (!m.found) { + return this + } + this.soften() + return m.replaceWith(input, keep) +} +export default fns diff --git a/tests/02-two/change/replace.test.js b/tests/02-two/change/replace.test.js new file mode 100644 index 0000000..434d9df --- /dev/null +++ b/tests/02-two/change/replace.test.js @@ -0,0 +1,227 @@ +import test from 'tape' +import nlp from '../../lib/two.js' +const here = '[two/change/replace] ' + +test('replace-basic :', function (t) { + let m = nlp('the dog played').match('dog').replace('cat').all() + t.equal(m.out('text'), 'the cat played', here + 'dog-cat') + + m = nlp('the dog played').match('the dog').replace('a cat').all() + t.equal(m.out('text'), 'a cat played', here + 'a-cat') + + m = nlp('the dog played').match('#Noun').replace('snake').all() + t.equal(m.out('text'), 'the snake played', here + 'snake') + + m = nlp('the pit bull played').match('#Noun+').replace('snake').all() + t.equal(m.out('text'), 'the snake played', here + 'pit bull') + + m = nlp('the pit bull dog played').match('#Noun+').replace('grey snake').all() + t.equal(m.out('text'), 'the grey snake played', here + 'pit bull dog') + + t.end() +}) + +test('match-replace :', function (t) { + const arr = [ + ['the dog played', 'the dog', 'the cat', 'the cat played'], + ['the dog played', 'the #Noun', 'the cat', 'the cat played'], + ['the dog played', 'the (dog|hamster|pet-snake)', 'the cat', 'the cat played'], + ['the boy and the girl', 'the #Noun', 'the house', 'the house and the house'], + ['the boy and the girl', 'the cat', 'the house', 'the boy and the girl'], + ['the boy and the girl', '(boy|girl)', 'cat', 'the cat and the cat'], + ] + arr.forEach(function (a) { + const str = nlp(a[0]).replace(a[1], a[2]).all().out('text') + const msg = str + ' -- ' + a[3] + t.equal(str, a[3], here + msg) + }) + t.end() +}) + +test('replace-with-punctuation', function (t) { + const doc = nlp('Simon, how is Pamela and Jason?') + const str = doc.match('#Person').replace('PERSON').all().out() + t.equal(str, 'PERSON, how is PERSON and PERSON?', here + 'replace-with-punctuation') + t.end() +}) + +test('structured-object-replace :', function (t) { + let r = nlp('fun times in cool town') + const term = r.match('times') + r.replace(term, 'day') + t.equal(r.out(), 'fun day in cool town', here + 'structured-replace') + + r = nlp('fun times in cool town') + const terms = r.match('cool town') + r.replace(terms, 'shitsville') + t.equal(r.out(), 'fun times in shitsville', here + 'structured-replace-multi') + t.end() +}) + +test('replace-keep some punctuation', function (t) { + let doc = nlp('two weeks').tag('Cool') + doc.replace('two', '2', { tags: true }) + t.equal(doc.match('#Cool+').text(), '2 weeks', here + 'replace-keep-tags') + + doc = nlp('first sentence. I am trying it out.') + const m = doc.match('trying').tag('HashTag') + m.replaceWith('working', { tags: true }) + t.equal(doc.match('#HashTag+').text(), 'working', here + 'replacewith-keep-tags') + t.end() +}) + +test('replace over implict', function (t) { + const doc = nlp("i'm good") + doc.match('am').replaceWith('was') + t.equal(doc.text(), 'i was good', here + 'replace over implicit') + t.end() +}) + +test('replace-with-Doc', function (t) { + const b = nlp('sneaks').tag('Cool') + const doc = nlp(`john walks quickly`) + doc.match('walks').replaceWith(b, { tags: true }) + t.equal(doc.text(), 'john sneaks quickly', here + 'doc-replace') + t.equal(doc.has('#Cool'), true, here + 'doc-replace tags') + t.end() +}) + +test('replace-with-function', function (t) { + const fn = p => { + if (p.has('john')) { + return 'johnny' + } + return 'nancy' + } + let doc = nlp('spencer and John') + doc.replace('#Person', fn) + t.equal(doc.text(), 'nancy and johnny', here + 'replace function') + + doc = nlp('spencer and John') + doc.match('#Person').replaceWith(fn, { case: true }) + t.equal(doc.text(), 'nancy and Johnny') + + doc = nlp('Thurs, Feb 2nd, 2016') + doc.match('feb').replaceWith(m => { + return m.text({ trim: true }) + '!' + }) + t.equal(doc.text(), 'Thurs, Feb! 2nd, 2016', here + 'replaceWith function') + + doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith((m) => m.text() + 's') + doc.match('is').replaceWith(() => 'are') + t.equal(doc.text(), 'Spencers are very cool.', here + 'replaceWith twice') + t.end() +}) + +test('replace-with-possessives', function (t) { + const fn = p => { + if (p.has('john')) { + return 'johnny' + } + return 'nancy' + } + const doc = nlp('spencer\'s house is cooler than John\'s house.') + doc.replace('#Person', fn, { case: true, possessives: true }) + t.equal(doc.text(), 'nancy\'s house is cooler than Johnny\'s house.') + t.end() +}) + +test('replace-tags-param', function (t) { + let doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith('jogging') + const m = doc.match('jogging') + t.equal(m.has('#Person'), false, here + 'tags not-kept - default') + + doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith('jogging', { tags: true }) + t.equal(doc.has('(jogging && #Person)'), true, here + 'tags kept - boolean') + t.equal(doc.has('(jogging && #Gerund)'), false, here + 'tags kept - boolean') + + doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith('jogging', { tags: true }) + t.equal(doc.has('(jogging && #Person)'), true, here + 'tags kept - boolean') + t.equal(doc.has('(jogging && #Gerund)'), false, 'here+tags kept - boolean') + + t.end() +}) + +test('replace-case-param', function (t) { + let doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith('jogging', { case: true }) + t.equal(doc.text(), 'Jogging is very cool.', here + here + 'case kept ') + + doc = nlp('spencer is very cool.') + doc.match('spencer').replaceWith('jogging', { case: false }) // Jogging? + t.equal(doc.text(), 'jogging is very cool.', here + here + 'lowsercase kept - default') + + doc = nlp('Spencer is very cool.') + doc.match('spencer').replaceWith('jogging', { case: false }) + t.equal(doc.text(), 'jogging is very cool.', 'dont-keep') + t.end() +}) + +test('replace-repair', function (t) { + let doc = nlp('the cat and the dog') + doc.replace('#Noun', 'house') + t.equal(doc.text(), 'the house and the house', here + 'repair-one-one') + + + doc = nlp('the cat and the dog') + doc.replace('the #Noun', 'the house') + t.equal(doc.text(), 'the house and the house', here + 'repair-two-two') + + doc = nlp('the cat and the dog') + doc.replace('#Noun', 'cool house') + t.equal(doc.text(), 'the cool house and the cool house', here + 'repair-one-two') + + doc = nlp('the cat and the dog') + doc.replace('the #Noun', 'house') + t.equal(doc.text(), 'house and house', here + 'repair-two-one') + t.end() +}) + +test('replace is cloned', function (t) { + const doc = nlp('before match after. second sentence here.') + const m = doc.match('match') + doc.match('sentence').replace(m) + t.equal(doc.text(), 'before match after. second match here.', here + 'replaced-text') + + const id = m.docs[0][0].id + const foundIds = doc.termList().filter(term => term.id === id) + t.equal(foundIds.length, 1, 'id-different') + + t.equal(m.length, 1, 'match-unchanged') + m.tag('NewTag') + t.equal(m.match('#NewTag').length, 1, 'tags-are-cloned') + + t.end() +}) + +test('replace-with-no-match', function (t) { + const doc = nlp('original') + doc.match('missing').replaceWith('unreached') + t.equal(doc.text(), 'original') + + doc.match('missing').replaceWith('unreached', { tags: true, possessives: true, case: true }) + t.equal(doc.text(), 'original') + t.end() +}) + +test('replace-with-pre-post', function (t) { + let doc = nlp('`Target`') + doc.match('Target').replaceWith('Replacement') + t.equal(doc.text(), '`Replacement`') + + doc = nlp('`John\'s`') + doc.match('John').replaceWith('Spencer', { possessives: true }) + t.equal(doc.text(), '`Spencer\'s`') + + doc = nlp('the `pit bull` played').match('#Noun+').replace('snake').all() + t.equal(doc.text(), 'the `snake` played') + + doc = nlp('the `snake` played').match('#Noun+').replace('pit bull').all() + t.equal(doc.text(), 'the `pit bull` played') + + t.end() +})