@@ -51,6 +51,14 @@ function isNewline(c: string) {
5151 return c === '\n' ;
5252}
5353
54+ function isWordSeparator ( c : string ) : boolean {
55+ // Specs say to treat U+1361, Ethiopic Word Space, as a word separator, but
56+ // digitized Ethiopic usually has spaces. Browsers don't support it either;
57+ // doing so would be complicated since the ink has to be centered. Specs also
58+ // say to handle Agean, Ugaritic, and Phoenician, but these are extinct.
59+ return c === ' ' || c === '\u00a0' ;
60+ }
61+
5462function graphemeBoundaries ( text : string , index : number ) {
5563 const graphemeEnd = nextGraphemeBreak ( text , index ) ;
5664 const graphemeStart = previousGraphemeBreak ( text , graphemeEnd ) ;
@@ -122,6 +130,10 @@ export class Run extends TreeNode {
122130 parent . bitfield |= Run . TEXT_BITS ;
123131 }
124132
133+ if ( this . style . wordSpacing !== 'normal' ) {
134+ parent . bitfield |= Box . BITS . hasWordSpacing ;
135+ }
136+
125137 for ( let i = this . textStart ; i < this . textEnd ; i ++ ) {
126138 const code = paragraph . charCodeAt ( i ) ;
127139
@@ -561,6 +573,44 @@ export const EmptyInlineMetrics: Readonly<InlineMetrics> = Object.freeze({
561573 descenderBox : 0
562574} ) ;
563575
576+ class WordIterator {
577+ item : ShapedItem ;
578+ textEnd : number ;
579+ state : MeasureState ;
580+ /* out */
581+ start : number ;
582+ end : number ;
583+ x : number ;
584+ w : number ;
585+ done : boolean ;
586+
587+ constructor ( item : ShapedItem , textStart : number , textEnd : number ) {
588+ this . item = item ;
589+ this . textEnd = textEnd ;
590+ this . state = item . createMeasureState ( ) ;
591+ this . start = textStart ;
592+ this . end = textStart ;
593+ this . x = 0 ;
594+ this . w = 0 ;
595+ this . done = false ;
596+ item . measure ( textStart , 1 , this . state ) ;
597+ this . next ( ) ;
598+ }
599+
600+ next ( ) {
601+ const s = this . item . ifc . text ;
602+ while ( this . end < this . textEnd && isWordSeparator ( s [ this . end ] ) ) this . end ++ ;
603+ this . start = this . end ;
604+ while ( this . end < this . textEnd && ! isWordSeparator ( s [ this . end ] ) ) this . end ++ ;
605+ if ( this . start < this . end ) {
606+ this . x = this . item . measure ( this . start , 1 , this . state ) . advance ;
607+ this . w = this . item . measure ( this . end , 1 , this . state ) . advance ;
608+ } else {
609+ this . done = true ;
610+ }
611+ }
612+ }
613+
564614export class ShapedItem implements IfcRenderItem {
565615 ifc : BlockContainerOfInlines ;
566616 face : LoadedFontFace ;
@@ -804,6 +854,20 @@ export class ShapedItem implements IfcRenderItem {
804854 return ci > this . offset && ci < this . end ( ) ;
805855 }
806856
857+ createWordIterator ( textStart : number , textEnd : number ) {
858+ return new WordIterator ( this , textStart , textEnd ) ;
859+ }
860+
861+ mayHaveModifiedWordSepGlyphs ( layout : Layout ) {
862+ if ( this . inlines . length ) {
863+ return this . inlines [ this . inlines . length - 1 ] . hasWordSpacing ( ) ;
864+ } else {
865+ const rootInline = layout . tree [ this . ifc . treeStart + 1 ] ;
866+ if ( ! rootInline . isInline ( ) ) throw new Error ( 'Assertion failed' ) ;
867+ return rootInline . hasWordSpacing ( ) ;
868+ }
869+ }
870+
807871 // only use this in debugging or tests
808872 text ( ) {
809873 return this . ifc . text . slice ( this . offset , this . offset + this . length ) ;
@@ -1832,6 +1896,63 @@ function postShapeLoadHyphens(ifc: BlockContainerOfInlines, items: ShapedItem[])
18321896 }
18331897}
18341898
1899+ function postShapeAddWordSpacing (
1900+ layout : Layout ,
1901+ ifc : BlockContainerOfInlines ,
1902+ items : ShapedItem [ ] ,
1903+ inlineIndex : number ,
1904+ itemIndex : number ,
1905+ endItem : number
1906+ ) {
1907+ while ( inlineIndex <= ifc . treeFinal && itemIndex < endItem ) {
1908+ const box = layout . tree [ inlineIndex ] ;
1909+ if ( box . isRun ( ) && box . style . wordSpacing !== 'normal' ) {
1910+ while ( // Forward to the item that owns the textOffset
1911+ itemIndex + 1 < endItem &&
1912+ items [ itemIndex + 1 ] . offset <= box . textStart
1913+ ) itemIndex ++ ;
1914+
1915+ while (
1916+ itemIndex < endItem &&
1917+ items [ itemIndex ] . offset < box . textEnd
1918+ ) {
1919+ const item = items [ itemIndex ] ;
1920+ const { wordSpacing, fontSize} = box . style ;
1921+ let addPx = typeof wordSpacing === 'number' ? wordSpacing
1922+ : wordSpacing . value / 100 * fontSize ;
1923+
1924+ const addUnits = addPx * item . face . hbface . upem / fontSize ;
1925+
1926+ // TODO this isn't... super great, iterating the same glyphs array
1927+ // multiple times if multiple inlines cover it, but this is typically
1928+ // extremely fast, plus inline word-spacing is probably rare. Still,
1929+ // if/when looking at generalizing glyph/span walking, try to improve.
1930+ for ( let i = 0 ; i < item . glyphs . length ; i = nextCluster ( item . glyphs , i ) ) {
1931+ const cl = item . glyphs [ i + G_CL ] ;
1932+ if ( isWordSeparator ( item . ifc . text [ cl ] ) ) {
1933+ if ( cl >= box . textStart && cl < box . textEnd ) {
1934+ item . glyphs [ i + G_AX ] += addUnits ;
1935+ }
1936+ }
1937+ }
1938+
1939+ if ( items [ itemIndex ] . end ( ) <= box . textEnd ) {
1940+ itemIndex ++ ;
1941+ } else {
1942+ break ; // spans into next inline
1943+ }
1944+ }
1945+ } else if ( box . isBox ( ) ) {
1946+ if ( box . isInline ( ) && box . hasWordSpacing ( ) ) {
1947+ // descend
1948+ } else {
1949+ inlineIndex = box . treeFinal ; // skip
1950+ }
1951+ }
1952+ inlineIndex ++ ;
1953+ }
1954+ }
1955+
18351956function shapePart (
18361957 ifc : BlockContainerOfInlines ,
18371958 offset : number ,
@@ -1988,6 +2109,9 @@ export function createIfcShapedItems(
19882109 items . sort ( ( a , b ) => a . offset - b . offset ) ;
19892110
19902111 if ( inlineRoot . hasSoftHyphen ( ) ) postShapeLoadHyphens ( ifc , items ) ;
2112+ if ( inlineRoot . hasWordSpacing ( ) ) {
2113+ postShapeAddWordSpacing ( layout , ifc , items , ifc . treeStart + 2 , 0 , items . length ) ;
2114+ }
19912115
19922116 return items ;
19932117}
@@ -2199,16 +2323,26 @@ export function getIfcContribution(
21992323 return contribution ;
22002324}
22012325
2202- function splitItem ( ifc : BlockContainerOfInlines , itemIndex : number , offset : number ) {
2326+ function splitItem (
2327+ layout : Layout ,
2328+ ifc : BlockContainerOfInlines ,
2329+ itemIndex : number ,
2330+ offset : number
2331+ ) {
22032332 const left = ifc . items [ itemIndex ] ;
22042333 const { needsReshape, right} = left . split ( offset - left . offset ) ;
22052334
2335+ ifc . items . splice ( itemIndex + 1 , 0 , right ) ;
2336+
22062337 if ( needsReshape ) {
2338+ const inlineIndex = ifc . getRunIndex ( layout , left . offset ) ;
22072339 left . reshape ( true ) ;
22082340 right . reshape ( false ) ;
2341+ if ( left . mayHaveModifiedWordSepGlyphs ( layout ) ) {
2342+ postShapeAddWordSpacing ( layout , ifc , ifc . items , inlineIndex , itemIndex , itemIndex + 2 ) ;
2343+ }
22092344 }
22102345
2211- ifc . items . splice ( itemIndex + 1 , 0 , right ) ;
22122346 if ( ifc . text [ offset - 1 ] === '\u00ad' /* softHyphenCharacter */ ) {
22132347 const hyphen = getHyphen ( left ) ?. glyphs ;
22142348 if ( hyphen ?. length ) {
@@ -2387,7 +2521,7 @@ export function createIfcLineboxes(
23872521 const lastBreakMarkItem = ifc . items [ lastBreakMark . itemIndex ] ;
23882522
23892523 if ( lastBreakMarkItem ?. hasCharacterInside ( lastBreakMark . position ) ) {
2390- splitItem ( ifc , lastBreakMark . itemIndex , lastBreakMark . position ) ;
2524+ splitItem ( layout , ifc , lastBreakMark . itemIndex , lastBreakMark . position ) ;
23912525 lastBreakMark . split ( mark ) ;
23922526 candidates . unshift ( ifc . items [ lastBreakMark . itemIndex ] ) ;
23932527 // Stamp the brand new line with its metrics, since the only other
0 commit comments