@@ -70,13 +70,13 @@ function isAllLower(w) {
7070}
7171
7272/**
73- * True if this position is exempt from lowercase (first/last/subphrase start/hyphen compound start).
74- * @param {{ isFirst: boolean, isLast: boolean, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean } } opts
73+ * True if this position is exempt from lowercase (first/last/subphrase start/hyphen compound start/phase label ).
74+ * @param {{ isFirst: boolean, isLast: boolean, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean, isPhaseLabel?: boolean } } opts
7575 * @returns {boolean }
7676 */
7777function isExemptFromLowercase ( opts ) {
78- const { isFirst, isLast, isSubphraseStart, isHyphenCompoundStart } = opts ;
79- return Boolean ( isFirst || isLast || isSubphraseStart || isHyphenCompoundStart ) ;
78+ const { isFirst, isLast, isSubphraseStart, isHyphenCompoundStart, isPhaseLabel } = opts ;
79+ return Boolean ( isFirst || isLast || isSubphraseStart || isHyphenCompoundStart || isPhaseLabel ) ;
8080}
8181
8282/**
@@ -93,7 +93,7 @@ function getCapitalizationKind(opts) {
9393
9494/**
9595 * Validate one word for title case; returns error detail string or null if valid.
96- * @param {{ raw: string, core: string, isFirst: boolean, isLast: boolean, lowercaseWords: Set<string>, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean } } opts
96+ * @param {{ raw: string, core: string, isFirst: boolean, isLast: boolean, lowercaseWords: Set<string>, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean, isPhaseLabel?: boolean } } opts
9797 * @returns {string|null }
9898 */
9999function checkWord ( opts ) {
@@ -111,7 +111,7 @@ function checkWord(opts) {
111111 * Return the corrected segment text for fixInfo: lowercase or capitalize per AP rules, preserving punctuation.
112112 * @param {string } rawSeg - Segment as in source (e.g. "And", "(in", "practice)")
113113 * @param {string } core - stripWordPunctuation(rawSeg)
114- * @param {{ isFirst: boolean, isLast: boolean, lowercaseWords: Set<string>, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean } } opts
114+ * @param {{ isFirst: boolean, isLast: boolean, lowercaseWords: Set<string>, isSubphraseStart?: boolean, isHyphenCompoundStart?: boolean, isPhaseLabel?: boolean } } opts
115115 * @returns {string }
116116 */
117117function getCorrectedSegment ( rawSeg , core , opts ) {
@@ -140,13 +140,24 @@ function getSegmentPosition(rawSegments, j) {
140140 return { segmentOffset, segmentLength : rawSegments [ j ] . length } ;
141141}
142142
143+ /**
144+ * True when the segment is a single letter immediately after the word "Phase" (phase label).
145+ * @param {number } j - Segment index (must be 0 for first segment of word)
146+ * @param {string } core - stripWordPunctuation(segment)
147+ * @param {string } previousWordCore - Previous word core, lowercased
148+ * @returns {boolean }
149+ */
150+ function isPhaseLabelSegment ( j , core , previousWordCore ) {
151+ return j === 0 && core . length === 1 && / ^ [ A - Z a - z ] $ / . test ( core ) && previousWordCore === "phase" ;
152+ }
153+
143154/**
144155 * Check one segment of a word for title-case; returns error result or null.
145- * @param {{ words: string[], rawSegments: string[], i: number, j: number, wordIsSubphraseStart: boolean, lowercaseWords: Set<string> } } opts
156+ * @param {{ words: string[], rawSegments: string[], i: number, j: number, wordIsSubphraseStart: boolean, lowercaseWords: Set<string>, previousWordCore?: string } } opts
146157 * @returns {{ valid: false, detail: string, wordIndex: number, segmentOffset?: number, segmentLength?: number }|null }
147158 */
148159function checkOneSegment ( opts ) {
149- const { words, rawSegments, i, j, wordIsSubphraseStart, lowercaseWords } = opts ;
160+ const { words, rawSegments, i, j, wordIsSubphraseStart, lowercaseWords, previousWordCore } = opts ;
150161 const rawSeg = rawSegments [ j ] ;
151162 const core = stripWordPunctuation ( rawSeg ) ;
152163 if ( ! core || ! / [ a - z A - Z ] / . test ( core ) ) return null ;
@@ -155,6 +166,7 @@ function checkOneSegment(opts) {
155166 const isLast = i === words . length - 1 && j === rawSegments . length - 1 ;
156167 const isSubphraseStart = j === 0 && wordIsSubphraseStart ;
157168 const isHyphenCompoundStart = rawSegments . length > 1 && j === 0 ;
169+ const isPhaseLabel = isPhaseLabelSegment ( j , core , previousWordCore ) ;
158170
159171 const detail = checkWord ( {
160172 raw : rawSeg ,
@@ -164,6 +176,7 @@ function checkOneSegment(opts) {
164176 lowercaseWords,
165177 isSubphraseStart,
166178 isHyphenCompoundStart,
179+ isPhaseLabel,
167180 } ) ;
168181 if ( ! detail ) return null ;
169182
@@ -173,6 +186,7 @@ function checkOneSegment(opts) {
173186 lowercaseWords,
174187 isSubphraseStart,
175188 isHyphenCompoundStart,
189+ isPhaseLabel,
176190 } ) ;
177191 const seg = getSegmentPosition ( rawSegments , j ) ;
178192 return {
@@ -206,6 +220,7 @@ function checkTitleCase(titleText, lowercaseWords) {
206220 const prefix = firstAlphaIdx > 0 ? raw . slice ( 0 , firstAlphaIdx ) : "" ;
207221 const afterColon = i > 0 && words [ i - 1 ] . replace ( / \s + $ / , "" ) . endsWith ( ":" ) ;
208222 const wordIsSubphraseStart = prefix . includes ( "(" ) || prefix . includes ( "[" ) || afterColon ;
223+ const previousWordCore = i > 0 ? stripWordPunctuation ( words [ i - 1 ] ) . toLowerCase ( ) : "" ;
209224
210225 const rawSegments = raw . split ( / - / ) ;
211226 for ( let j = 0 ; j < rawSegments . length ; j ++ ) {
@@ -216,6 +231,7 @@ function checkTitleCase(titleText, lowercaseWords) {
216231 j,
217232 wordIsSubphraseStart,
218233 lowercaseWords,
234+ previousWordCore,
219235 } ) ;
220236 if ( result ) errors . push ( { detail : result . detail , wordIndex : result . wordIndex , insertText : result . insertText , segmentOffset : result . segmentOffset , segmentLength : result . segmentLength } ) ;
221237 }
@@ -245,21 +261,23 @@ function getLowercaseWordsFromOptions(options) {
245261 * Correct one segment of a word for AP title case.
246262 * @param {string } rawSeg - Segment as in source
247263 * @param {string } core - stripWordPunctuation(rawSeg)
248- * @param {{ i: number, j: number, words: string[], rawSegments: string[], wordIsSubphraseStart: boolean, lowercaseWords: Set<string> } } ctx
264+ * @param {{ i: number, j: number, words: string[], rawSegments: string[], wordIsSubphraseStart: boolean, lowercaseWords: Set<string>, previousWordCore?: string } } ctx
249265 * @returns {string }
250266 */
251267function correctSegmentForTitleCase ( rawSeg , core , ctx ) {
252- const { i, j, words, rawSegments, wordIsSubphraseStart, lowercaseWords } = ctx ;
268+ const { i, j, words, rawSegments, wordIsSubphraseStart, lowercaseWords, previousWordCore } = ctx ;
253269 const isFirst = i === 0 && j === 0 ;
254270 const isLast = i === words . length - 1 && j === rawSegments . length - 1 ;
255271 const isSubphraseStart = j === 0 && wordIsSubphraseStart ;
256272 const isHyphenCompoundStart = rawSegments . length > 1 && j === 0 ;
273+ const isPhaseLabel = isPhaseLabelSegment ( j , core , previousWordCore ) ;
257274 return getCorrectedSegment ( rawSeg , core , {
258275 isFirst,
259276 isLast,
260277 lowercaseWords,
261278 isSubphraseStart,
262279 isHyphenCompoundStart,
280+ isPhaseLabel,
263281 } ) ;
264282}
265283
@@ -276,6 +294,7 @@ function processWordForTitleCase(raw, i, words, lowercaseWords) {
276294 const prefix = firstAlphaIdx > 0 ? raw . slice ( 0 , firstAlphaIdx ) : "" ;
277295 const afterColon = i > 0 && words [ i - 1 ] . replace ( / \s + $ / , "" ) . endsWith ( ":" ) ;
278296 const wordIsSubphraseStart = prefix . includes ( "(" ) || prefix . includes ( "[" ) || afterColon ;
297+ const previousWordCore = i > 0 ? stripWordPunctuation ( words [ i - 1 ] ) . toLowerCase ( ) : "" ;
279298 const rawSegments = raw . split ( / - / ) ;
280299 const segmentParts = [ ] ;
281300 for ( let j = 0 ; j < rawSegments . length ; j ++ ) {
@@ -292,6 +311,7 @@ function processWordForTitleCase(raw, i, words, lowercaseWords) {
292311 rawSegments,
293312 wordIsSubphraseStart,
294313 lowercaseWords,
314+ previousWordCore,
295315 } ) ) ;
296316 }
297317 return segmentParts . join ( "-" ) ;
0 commit comments