-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Represent linear offset in XML-like content using range
This commit introduces offsetOf to express linear positioning in XML-like content using range. It allows for clear differentiation between content inside and outside of open and end tags, similar to the indexing system used in ProseMirror.
- Loading branch information
1 parent
3365f92
commit cfb38f5
Showing
7 changed files
with
71 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,6 @@ dist-ssr | |
*.njsproj | ||
*.sln | ||
*.sw? | ||
|
||
# __screenshots__ | ||
__screenshots__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,56 @@ | ||
import { Range } from '../range.ts'; | ||
|
||
export type Position = [Node, number]; | ||
|
||
/** | ||
* `offsetOf` returns the offset of the node in the container. | ||
*/ | ||
function offsetOf(node: Node, container: Node): number { | ||
let offset = 0; | ||
let current = node.previousSibling; | ||
export function offsetOf(pos: Position, container: Node): number { | ||
const [node, offset] = pos; | ||
|
||
while (current) { | ||
offset += current.textContent?.length || 0; | ||
current = current.previousSibling; | ||
} | ||
let found = false; | ||
function visit(n: Node): number { | ||
if (n === node) { | ||
found = true; | ||
return offset; | ||
} | ||
|
||
if (node.parentNode !== container) { | ||
if (!node.parentNode) { | ||
throw new Error('node is not in the container'); | ||
if (n.nodeType === Node.TEXT_NODE) { | ||
return n.textContent!.length; | ||
} | ||
|
||
return offset + offsetOf(node.parentNode, container); | ||
// Add 1 for the open tag. | ||
let sum = 1; | ||
for (const child of n.childNodes) { | ||
sum += visit(child); | ||
if (found) { | ||
return sum; | ||
} | ||
} | ||
|
||
// Add 1 for the close tag. | ||
return sum + 1; | ||
} | ||
|
||
// NOTE(hackerwins): Subtract 1 because the open tag of the container is not | ||
// included in the offset. | ||
const result = visit(container) - 1; | ||
if (!found) { | ||
throw new Error('node is not in the container'); | ||
} | ||
|
||
return offset; | ||
return result; | ||
} | ||
|
||
/** | ||
* `toRange` converts the abstract range to the range. | ||
*/ | ||
export function toRange(range: AbstractRange, container: Node): Range { | ||
const start = offsetOf(range.startContainer, container) + range.startOffset; | ||
const start = offsetOf([range.startContainer, range.startOffset], container); | ||
if (range.collapsed) { | ||
return { s: start, e: start }; | ||
} | ||
|
||
const end = offsetOf(range.endContainer, container) + range.endOffset; | ||
const end = offsetOf([range.endContainer, range.endOffset], container); | ||
return { s: start, e: end }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,27 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { toRange } from '../../src/view/selection'; | ||
import { offsetOf } from '../../src/view/selection'; | ||
|
||
describe('toRange', () => { | ||
it('should convert to the range', () => { | ||
const container = document.createElement('div'); | ||
container.innerHTML = 'Hello, World!'; | ||
|
||
const range = document.createRange(); | ||
range.setStart(container.firstChild!, 1); | ||
range.setEnd(container.firstChild!, 4); | ||
|
||
const result = toRange(range, container); | ||
expect(result).toEqual({ s: 1, e: 4 }); | ||
describe('offsetOf', () => { | ||
it('should convert to the given position', () => { | ||
const d = document.createElement('div'); | ||
d.innerHTML = 'Hello World!'; | ||
expect(offsetOf([d.firstChild!, 1], d)).toEqual(1); | ||
}); | ||
|
||
it('should convert to the range in the nested element', () => { | ||
const container = document.createElement('div'); | ||
container.innerHTML = 'Hello, <b>World</b>!'; | ||
|
||
const range = document.createRange(); | ||
range.setStart(container.firstChild!, 1); | ||
range.setEnd(container.querySelector('b')!.firstChild!, 2); | ||
|
||
const result = toRange(range, container); | ||
expect(result).toEqual({ s: 1, e: 9 }); | ||
it('should convert to the given position in element', () => { | ||
const d = document.createElement('div'); | ||
d.innerHTML = '<b>Hello</b> <i>World</i>!'; | ||
expect(offsetOf([d.querySelector('b')!.firstChild!, 0], d)).toEqual(1); | ||
expect(offsetOf([d.querySelector('i')!.firstChild!, 0], d)).toEqual(9); | ||
expect(offsetOf([d.querySelector('i')!.firstChild!, 1], d)).toEqual(10); | ||
expect(offsetOf([d.querySelector('i')!.nextSibling!, 0], d)).toEqual(15); | ||
}); | ||
|
||
it('should return error if the range is not in the container', () => { | ||
const container = document.createElement('div'); | ||
container.innerHTML = 'Hello, World!'; | ||
|
||
const range = document.createRange(); | ||
range.setStart(container.firstChild!, 1); | ||
range.setEnd(container.firstChild!, 4); | ||
|
||
expect(() => toRange(range, document.createElement('div'))).toThrowError( | ||
'node is not in the container', | ||
); | ||
const d = document.createElement('div'); | ||
d.innerHTML = 'Hello World!'; | ||
expect(() => | ||
offsetOf([d.firstChild!, 4], document.createElement('div')), | ||
).toThrowError('node is not in the container'); | ||
}); | ||
}); |