@@ -1211,16 +1211,16 @@ export const Editor: EditorInterface = {
12111211 } ,
12121212
12131213 /**
1214- * Iterate through all of the positions in the document where a `Point` can be
1215- * placed.
1214+ * Return all the positions in `at` range where a `Point` can be placed.
12161215 *
1217- * By default it will move forward by individual offsets at a time, but you
1218- * can pass the `unit: 'character'` option to moved forward one character, word,
1219- * or line at at time.
1216+ * By default, moves forward by individual offsets at a time, but
1217+ * the `unit` option can be used to to move by character, word, line, or block.
1218+ *
1219+ * The `reverse` option can be used to change iteration direction.
12201220 *
12211221 * Note: By default void nodes are treated as a single point and iteration
12221222 * will not happen inside their content unless you pass in true for the
1223- * voids option, then iteration will occur.
1223+ * ` voids` option, then iteration will occur.
12241224 */
12251225
12261226 * positions (
@@ -1243,100 +1243,160 @@ export const Editor: EditorInterface = {
12431243 return
12441244 }
12451245
1246+ /**
1247+ * Algorithm notes:
1248+ *
1249+ * Each step `distance` is dynamic depending on the underlying text
1250+ * and the `unit` specified. Each step, e.g., a line or word, may
1251+ * span multiple text nodes, so we iterate through the text both on
1252+ * two levels in step-sync:
1253+ *
1254+ * `leafText` stores the text on a text leaf level, and is advanced
1255+ * through using the counters `leafTextOffset` and `leafTextRemaining`.
1256+ *
1257+ * `blockText` stores the text on a block level, and is shortened
1258+ * by `distance` every time it is advanced.
1259+ *
1260+ * We only maintain a window of one blockText and one leafText because
1261+ * a block node always appears before all of its leaf nodes.
1262+ */
1263+
12461264 const range = Editor . range ( editor , at )
12471265 const [ start , end ] = Range . edges ( range )
12481266 const first = reverse ? end : start
1249- let string = ''
1250- let available = 0
1251- let offset = 0
1252- let distance : number | null = null
12531267 let isNewBlock = false
1254-
1255- const advance = ( ) => {
1256- if ( distance == null ) {
1257- if ( unit === 'character' ) {
1258- distance = getCharacterDistance ( string )
1259- } else if ( unit === 'word' ) {
1260- distance = getWordDistance ( string )
1261- } else if ( unit === 'line' || unit === 'block' ) {
1262- distance = string . length
1263- } else {
1264- distance = 1
1265- }
1266-
1267- string = string . slice ( distance )
1268- }
1269-
1270- // Add or substract the offset.
1271- offset = reverse ? offset - distance : offset + distance
1272- // Subtract the distance traveled from the available text.
1273- available = available - distance !
1274- // If the available had room to spare, reset the distance so that it will
1275- // advance again next time. Otherwise, set it to the overflow amount.
1276- distance = available >= 0 ? null : 0 - available
1277- }
1278-
1268+ let blockText = ''
1269+ let distance = 0 // Distance for leafText to catch up to blockText.
1270+ let leafTextRemaining = 0
1271+ let leafTextOffset = 0
1272+
1273+ // Iterate through all nodes in range, grabbing entire textual content
1274+ // of block nodes in blockText, and text nodes in leafText.
1275+ // Exploits the fact that nodes are sequenced in such a way that we first
1276+ // encounter the block node, then all of its text nodes, so when iterating
1277+ // through the blockText and leafText we just need to remember a window of
1278+ // one block node and leaf node, respectively.
12791279 for ( const [ node , path ] of Editor . nodes ( editor , { at, reverse, voids } ) ) {
1280+ /*
1281+ * ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
1282+ */
12801283 if ( Element . isElement ( node ) ) {
12811284 // Void nodes are a special case, so by default we will always
1282- // yield their first point. If the voids option is set to true,
1283- // then we will iterate over their content
1285+ // yield their first point. If the ` voids` option is set to true,
1286+ // then we will iterate over their content.
12841287 if ( ! voids && editor . isVoid ( node ) ) {
12851288 yield Editor . start ( editor , path )
12861289 continue
12871290 }
12881291
1289- if ( editor . isInline ( node ) ) {
1290- continue
1291- }
1292+ // Inline element nodes are ignored as they don't themselves
1293+ // contribute to `blockText` or `leafText` - their parent and
1294+ // children do.
1295+ if ( editor . isInline ( node ) ) continue
12921296
1297+ // Block element node - set `blockText` to its text content.
12931298 if ( Editor . hasInlines ( editor , node ) ) {
1299+ // We always exhaust block nodes before encountering a new one:
1300+ // console.assert(blockText === '',
1301+ // `blockText='${blockText}' - `+
1302+ // `not exhausted before new block node`, path)
1303+
1304+ // Ensure range considered is capped to `range`, in the
1305+ // start/end edge cases where block extends beyond range.
1306+ // Equivalent to this, but presumably more performant:
1307+ // blockRange = Editor.range(editor, ...Editor.edges(editor, path))
1308+ // blockRange = Range.intersection(range, blockRange) // intersect
1309+ // blockText = Editor.string(editor, blockRange, { voids })
12941310 const e = Path . isAncestor ( path , end . path )
12951311 ? end
12961312 : Editor . end ( editor , path )
12971313 const s = Path . isAncestor ( path , start . path )
12981314 ? start
12991315 : Editor . start ( editor , path )
13001316
1301- const text = Editor . string ( editor , { anchor : s , focus : e } , { voids } )
1302- string = reverse ? reverseText ( text ) : text
1317+ blockText = Editor . string ( editor , { anchor : s , focus : e } , { voids } )
1318+ blockText = reverse ? reverseText ( blockText ) : blockText
13031319 isNewBlock = true
13041320 }
13051321 }
13061322
1323+ /*
1324+ * TEXT LEAF NODE - Iterate through text content, yielding
1325+ * positions every `distance` offset according to `unit`.
1326+ */
13071327 if ( Text . isText ( node ) ) {
13081328 const isFirst = Path . equals ( path , first . path )
1309- available = node . text . length
1310- offset = reverse ? available : 0
13111329
1330+ // Proof that we always exhaust text nodes before encountering a new one:
1331+ // console.assert(leafTextRemaining <= 0,
1332+ // `leafTextRemaining=${leafTextRemaining} - `+
1333+ // `not exhausted before new leaf text node`, path)
1334+
1335+ // Reset `leafText` counters for new text node.
13121336 if ( isFirst ) {
1313- available = reverse ? first . offset : available - first . offset
1314- offset = first . offset
1337+ leafTextRemaining = reverse
1338+ ? first . offset
1339+ : node . text . length - first . offset
1340+ leafTextOffset = first . offset // Works for reverse too.
1341+ } else {
1342+ leafTextRemaining = node . text . length
1343+ leafTextOffset = reverse ? leafTextRemaining : 0
13151344 }
13161345
1346+ // Yield position at the start of node (potentially).
13171347 if ( isFirst || isNewBlock || unit === 'offset' ) {
1318- yield { path, offset }
1348+ yield { path, offset : leafTextOffset }
1349+ isNewBlock = false
13191350 }
13201351
1352+ // Yield positions every (dynamically calculated) `distance` offset.
13211353 while ( true ) {
1322- // If there's no more string and there is no more characters to skip, continue to the next block.
1323- if ( string === '' && distance === null ) {
1324- break
1325- } else {
1326- advance ( )
1354+ // If `leafText` has caught up with `blockText` (distance=0),
1355+ // and if blockText is exhausted, break to get another block node,
1356+ // otherwise advance blockText forward by the new `distance`.
1357+ if ( distance === 0 ) {
1358+ if ( blockText === '' ) break
1359+ distance = calcDistance ( blockText , unit )
1360+ blockText = blockText . slice ( distance )
13271361 }
13281362
1329- // If the available space hasn't overflow, we have another point to
1330- // yield in the current text node.
1331- if ( available >= 0 ) {
1332- yield { path, offset }
1333- } else {
1363+ // Advance `leafText` by the current `distance`.
1364+ leafTextOffset = reverse
1365+ ? leafTextOffset - distance
1366+ : leafTextOffset + distance
1367+ leafTextRemaining = leafTextRemaining - distance
1368+
1369+ // If `leafText` is exhausted, break to get a new leaf node
1370+ // and set distance to the overflow amount, so we'll (maybe)
1371+ // catch up to blockText in the next leaf text node.
1372+ if ( leafTextRemaining < 0 ) {
1373+ distance = - leafTextRemaining
13341374 break
13351375 }
1336- }
13371376
1338- isNewBlock = false
1377+ // Successfully walked `distance` offsets through `leafText`
1378+ // to catch up with `blockText`, so we can reset `distance`
1379+ // and yield this position in this node.
1380+ distance = 0
1381+ yield { path, offset : leafTextOffset }
1382+ }
1383+ }
1384+ }
1385+ // Proof that upon completion, we've exahusted both leaf and block text:
1386+ // console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted")
1387+ // console.assert(blockText === '', "blockText wasn't exhausted")
1388+
1389+ // Helper:
1390+ // Return the distance in offsets for a step of size `unit` on given string.
1391+ function calcDistance ( text : string , unit : string ) {
1392+ if ( unit === 'character' ) {
1393+ return getCharacterDistance ( text )
1394+ } else if ( unit === 'word' ) {
1395+ return getWordDistance ( text )
1396+ } else if ( unit === 'line' || unit === 'block' ) {
1397+ return text . length
13391398 }
1399+ return 1
13401400 }
13411401 } ,
13421402
0 commit comments