import { normalizeText } from './normalize.js'; import { NUM10, SAME_CLAUSE, SCHEDULE } from './rules.js'; /** * Words of intent to die or to take too much (A1b, A1c, M1), the one list for * check symptoms and Ask (M2), with three outcomes (M-1): the self-harm sign, * only with a wish, plan, method or amount ("I want to die", "I hope I die in * my sleep", "Will I die if I take all of them?"), including someone else's * wish told by their family ("Mum keeps saying she wants to die", M-B); the * sign of feeling like dying, never labelled self-harm ("I feel like I'm * dying", "my baby is dying"); and never a sign: figures of speech ("my back * is killing me", "dying of thirst", "she would literally kill me"), * end-of-life talk ("She wants to die at home", "He is ready to die and has * stopped eating") and fear of death ("I don't want to die young"). The * self-harm words of the sign table live here too (review 7), so there is one * list. H-A: an amount of medicine taken, with a word about dying or killing * now, is too much medicine ("I'm going to die from all the pills I took"). */ const B = '(? text.split(/[.,;:!?\n]+/).map((clause) => normalizeText(clause)).filter(Boolean); /** H-A: the sentences of a text: commas don't end one ("The pills are going to kill me tonight, I've taken them all"). */ const sentencesOf = (text: string) => text.split(/[.!?;\n]+/).map((sentence) => normalizeText(sentence)).filter(Boolean); /** R9-M2: "is", "was" and the like end the words for a person ("my phone is dying" is never a person: every word is checked). */ const COPULA = `(?:is|was|are|were|am|be|been|being|has|had|keeps|kept|looks|looked|seems|seemed)${E}`; /** Not a person: "my phone is dying", "the battery dey die" (LOW), R10-L1: a pet ("my dog is dying"). */ const NOT_A_PERSON = `(?:phone|phones|battery|batteries|laptop|computer|ipad|tablet|mobile|torch|car|plant|plants|tree|trees|flowers?|garden|hair|business` + `|dog|dogs|cat|cats|puppy|kitten|pet|pets|horse|bird|rabbit|hamster|goat|chicken|parrot|fish)${E}`; /** R9-M2, R10-M1: a child, by any name: "my little one", "our youngest", "my grandson". */ const CHILD_NOUN = '(?:baby|babies|child|children|kid|kids|son|daughter|boy|girl|toddler|newborn|pikin|little one|little ones|youngest|eldest|grandson|granddaughter|grandchild|grandkid)'; /** Someone who could be dying: the speaker, anyone named ("my baby", "her dad", R9-M2: "my little boy", "our 3 year old"), the baby. */ const ANYONE = `(?:i|im|she|shes|he|hes|they|theyre|we|(?:my|her|his|their|our)(?: (?!${NOT_A_PERSON})(?!${COPULA})[a-z0-9]+){1,3}|the baby|baby)`; /** * M-1, H-1.4, M-c: feeling like they are dying, or someone dying now: the sign "Feeling like they are dying", never labelled * self-harm. "I feel like I'm dying", "I think I'm dying", "I think my baby is dying", "I'm frightened she's dying", "she looks * like she's dying", "I'm scared I'm going to die", "I'm dying", Pidgin "I dey die", "my pikin dey die", "Mama dey die", and * (M-B, R9-M2, R10-M1) a child dying ("my baby is dying", "my son is dying", "my little girl is dying", "my 2 year old is dying", "our * youngest is dying"). Only dying now ("is dying", "about to die", "going to die" after * fear words), never a death in the future ("I'm afraid I'll die if I stop my tablets", "my dad will die if he keeps drinking"). * Not "dying of thirst", "dying to", "dying my hair", "I'm dying here", "I'm dying lol", "my phone is dying", or an adult dying * (M-B: end-of-life talk, or the backstop). */ const DYING = new RegExp([ `${B}(?:feel|feels|felt|feeling|think|thinks|thought|look|looks|looked|looking|seem|seems|seemed|sound|sounds|sounded) like ${ANYONE}(?: is| am| are|m|s|re)? (?:dying|going to die|gonna die|about to die)${E}`, `${B}(?:think|thinks|thought|feel|feels|felt|fear|fears|feared|scared|afraid|worried|frightened|terrified)(?: that)? ${ANYONE}(?: is| am| are|m|s|re)? (?:dying|about to die|going to die|gonna die)${E}`, `${B}(?:i am|im) dying${E}(?! (?:of|for|to|my|her|his|their|the|a|an|inside|laughing|from|here|lol|haha|lmao|rn)${E})`, `${B}(?:i|she|he|e|mama|papa|baby|pikin|(?:my|her|his|di|the) (?!${NOT_A_PERSON})[a-z]+) dey die${E}`, `${B}(?:(?:my|our|her|his|the)(?: [a-z]+){0,2} ${CHILD_NOUN}|(?:my|our|her|his|the) [0-9]+ (?:year|month|week|day)s? old)` + `(?: is|s| are)? (?:dying(?! (?:of|for|to)${E})|about to die)${E}`, ].join('|')); /** Taking too much, or a poison: the overdose sign. */ const TOO_MUCH = new RegExp([ // Poisons, not food poisoning: Sniper and Gammalin (insecticides), Otapiapia (a rat poison). L5: not bleach on its own ("I clean // the toilet with bleach"); drinking or swallowing it is in the table (rules.ts). // LOW: not "poison ivy", "poison oak". '(? OTHER_WISH.test(clause) || OTHER_WISH_PLAIN.test(clause); export function intentIn(text: string | null | undefined): { selfHarm: boolean; tooMuch: boolean; dying: boolean; unsure: boolean } { const whole = normalizeText(text ?? ''); const clauses = clausesOf(text ?? ''); const endOfLife = END_OF_LIFE.test(whole); // M-B, R9-M1: someone else's wish to die is the self-harm sign: a child's always, an adult's unless the text has an explicit // end-of-life word (then the backstop, below). R9-M3: "and won't wake up" completes the speaker's own wish. R12-M1: the speaker's // "I will die" next to a prognosis is not intent, unless the clause says so. const selfHarm = SELF_HARM.test(whole) || clauses.some((clause) => SELF_HARM.test(clause) || SELF_HARM_CLAUSE.test(clause) || WAKE.test(clause) || MEDICINE_KILLS_ME_NOW.test(clause) || (otherWish(clause) && (CHILD.test(clause) || !EXPLICIT_END_OF_LIFE.test(whole))) || (FIRST_PERSON_WISH.test(clause) && WAKE_AFTER_AND.test(clause)) || (SPEAKER_DIE.test(clause) && !(PROGNOSIS.test(whole) && !DIE_INTENT.test(clause)))); // H-A: an amount of medicine taken and a word about dying or killing now, in one sentence. const tooMuch = TOO_MUCH.test(whole) || clauses.some((clause) => TOO_MUCH.test(clause)) || KEPT_THEN_TOOK.test(whole) || sentencesOf(text ?? '').some((sentence) => AMOUNT_TAKEN.test(sentence) && DEATH_NOW.test(sentence)); // Feeling like dying is its own sign only when it isn't self-harm: one label, never both. const dying = !selfHarm && DYING.test(whole); // H-A: a medicine that will kill me is never silent; M-B: an adult dying is no sign only with end-of-life talk; R9-H4: someone // else's wish to die is never silent, whatever the never-a-sign list says. // R11-H1, R12-H2: end-of-life talk silences a word about dying only without a word for wanting, asking or talking, or a "to die", in // the clause. const unsure = !selfHarm && !tooMuch && !dying && (clauses.some(otherWish) || clauses.some((clause) => DEATH_WORD.test(clause) && !NEVER_A_SIGN.test(clause) && !(EOL_TALK.test(clause) && !WISH_WORD.test(clause) && !TO_DIE.test(clause)) && (!KILL_STATEMENT.test(clause) || MEDICINE_KILLS.test(clause)) && !(endOfLife && ADULT_DYING.test(clause)))); return { selfHarm, tooMuch, dying, unsure }; } /** A word of intent (self-harm or too much): Ask's red card before anything else. Feeling like dying is a sign (screenSymptoms). */ export function hasIntent(text: string | null | undefined): boolean { const intent = intentIn(text); return intent.selfHarm || intent.tooMuch; } const NOT_ENGLISH_INTENT_WORDS = new RegExp(`${B}${NOT_ENGLISH_INTENT}${E}`, 'g'); /** * LOW: a normalized text without the Hausa, Yoruba and Igbo words of intent this list reads ("Mo fe ku"), so a text of only * those counts as read: the self-harm sign, without "these weren't checked". */ export const withoutWordsOfIntentRead = (normalized: string) => normalized.replace(NOT_ENGLISH_INTENT_WORDS, ' ').replace(/ +/g, ' ').trim();