import { resolve } from 'path' import { test } from 'uvu' import { equal, is, not, type } from 'uvu/assert' import postcss, { AnyNode, AtRule, CssSyntaxError, Declaration, Document, parse, Plugin, Result, Root, Rule } from '../lib/postcss.js' function stringify(node: AnyNode, builder: (str: string) => void): void { if (node.type === 'rule') { builder(node.selector) } } test('error() generates custom error', () => { let file = resolve('a.css') let css = parse('a{}', { from: file }) let a = css.first as Rule let error = a.error('Test') is(error instanceof CssSyntaxError, true) is(error.message, file + ':1:1: Test') }) test('error() generates custom error for nodes without source', () => { let rule = new Rule({ selector: 'a' }) let error = rule.error('Test') is(error.message, ': Test') }) test('error() highlights index', () => { let root = parse('a { b: c }') let a = root.first as Rule let b = a.first as Declaration let error = b.error('Bad semicolon', { index: 1 }) is(error.showSourceCode(false), '> 1 | a { b: c }\n' + ' | ^') }) test('error() highlights word', () => { let root = parse('a { color: x red }') let a = root.first as Rule let color = a.first as Declaration let error = color.error('Wrong color', { word: 'x' }) is( error.showSourceCode(false), '> 1 | a { color: x red }\n' + ' | ^' ) }) test('error() highlights word in multiline string', () => { let root = parse('a { color: red\n x }') let a = root.first as Rule let color = a.first as Declaration let error = color.error('Wrong color', { word: 'x' }) is( error.showSourceCode(false), ' 1 | a { color: red\n' + '> 2 | x }\n' + ' | ^' ) }) test('warn() attaches a warning to the result object', async () => { let warning: any let warner: Plugin = { Once(css, { result }) { warning = css.first?.warn(result, 'FIRST!') }, postcssPlugin: 'warner' } let result = await postcss([warner]).process('a{}', { from: undefined }) is(warning.type, 'warning') is(warning.text, 'FIRST!') is(warning.plugin, 'warner') equal(result.warnings(), [warning]) }) test('warn() accepts options', () => { let warner = (css: Root, result: Result): void => { css.first?.warn(result, 'FIRST!', { index: 1 }) } let result = postcss([warner]).process('a{}') is(result.warnings().length, 1) let warning = result.warnings()[0] as any is(warning.index, 1) }) test('remove() removes node from parent', () => { let rule = new Rule({ selector: 'a' }) let decl = new Declaration({ prop: 'color', value: 'black' }) rule.append(decl) decl.remove() is(rule.nodes.length, 0) type(decl.parent, 'undefined') }) test('replaceWith() inserts new node', () => { let rule = new Rule({ selector: 'a' }) rule.append({ prop: 'color', value: 'black' }) rule.append({ prop: 'width', value: '1px' }) rule.append({ prop: 'height', value: '1px' }) let node = new Declaration({ prop: 'min-width', value: '1px' }) let width = rule.nodes[1] let result = width.replaceWith(node) equal(result, width) is( rule.toString(), 'a {\n' + ' color: black;\n' + ' min-width: 1px;\n' + ' height: 1px\n' + '}' ) }) test('replaceWith() inserts new root', () => { let root = new Root() root.append(new AtRule({ name: 'import', params: '"a.css"' })) let a = new Root() a.append(new Rule({ selector: 'a' })) a.append(new Rule({ selector: 'b' })) root.first?.replaceWith(a) is(root.toString(), 'a {}\nb {}') }) test('replaceWith() replaces node', () => { let css = parse('a{one:1;two:2}') let a = css.first as Rule let one = a.first as Declaration let result = one.replaceWith({ prop: 'fix', value: 'fixed' }) is(result.prop, 'one') type(result.parent, 'undefined') is(css.toString(), 'a{fix:fixed;two:2}') }) test('replaceWith() can include itself', () => { let css = parse('a{one:1;two:2}') let a = css.first as Rule let one = a.first as Declaration let beforeDecl = { prop: 'fix1', value: 'fixedOne' } let afterDecl = { prop: 'fix2', value: 'fixedTwo' } one.replaceWith(beforeDecl, one, afterDecl) is(css.toString(), 'a{fix1:fixedOne;one:1;fix2:fixedTwo;two:2}') }) test('toString() accepts custom stringifier', () => { is(new Rule({ selector: 'a' }).toString(stringify), 'a') }) test('toString() accepts custom syntax', () => { is(new Rule({ selector: 'a' }).toString({ stringify }), 'a') }) test('assign() assigns to node', () => { let decl = new Declaration({ prop: 'white-space', value: 'overflow-wrap' }) is(decl.prop, 'white-space') is(decl.value, 'overflow-wrap') decl.assign({ prop: 'word-wrap', value: 'break-word' }) is(decl.prop, 'word-wrap') is(decl.value, 'break-word') }) test('clone() clones nodes', () => { let rule = new Rule({ selector: 'a' }) rule.append({ prop: 'color', value: '/**/black' }) let clone = rule.clone() type(clone.parent, 'undefined') equal(rule.first?.parent, rule) equal(clone.first?.parent, clone) clone.append({ prop: 'z-index', value: '1' }) is(rule.nodes.length, 1) }) test('clone() overrides properties', () => { let rule = new Rule({ selector: 'a' }) let clone = rule.clone({ selector: 'b' }) is(clone.selector, 'b') }) test('clone() keeps code style', () => { let css = parse('@page 1{a{color:black;}}') is(css.clone().toString(), '@page 1{a{color:black;}}') }) test('clone() works with null in raws', () => { let decl = new Declaration({ prop: 'color', // @ts-expect-error Testing invalid input raws: { value: null }, value: 'black' }) let clone = decl.clone() equal(Object.keys(clone.raws), ['value']) }) test('cloneBefore() clones and insert before current node', () => { let rule = new Rule({ raws: { after: '' }, selector: 'a' }) rule.append({ prop: 'z-index', raws: { before: '' }, value: '1' }) let result = rule.first?.cloneBefore({ value: '2' }) equal(result, rule.first) is(rule.toString(), 'a {z-index: 2;z-index: 1}') }) test('cloneAfter() clones and insert after current node', () => { let rule = new Rule({ raws: { after: '' }, selector: 'a' }) rule.append({ prop: 'z-index', raws: { before: '' }, value: '1' }) let result = rule.first?.cloneAfter({ value: '2' }) equal(result, rule.last) is(rule.toString(), 'a {z-index: 1;z-index: 2}') }) test('before() insert before current node', () => { let rule = new Rule({ raws: { after: '' }, selector: 'a' }) rule.append({ prop: 'z-index', raws: { before: '' }, value: '1' }) let result = rule.first?.before('color: black') equal(result, rule.last) is(rule.toString(), 'a {color: black;z-index: 1}') }) test('after() insert after current node', () => { let rule = new Rule({ raws: { after: '' }, selector: 'a' }) rule.append({ prop: 'z-index', raws: { before: '' }, value: '1' }) let result = rule.first?.after('color: black') equal(result, rule.first) is(rule.toString(), 'a {z-index: 1;color: black}') }) test('next() returns next node', () => { let css = parse('a{one:1;two:2}') let a = css.first as Rule equal(a.first?.next(), a.last) type(a.last?.next(), 'undefined') }) test('next() returns undefined on no parent', () => { let css = parse('') type(css.next(), 'undefined') }) test('prev() returns previous node', () => { let css = parse('a{one:1;two:2}') let a = css.first as Rule equal(a.last?.prev(), a.first) type(a.first?.prev(), 'undefined') }) test('prev() returns undefined on no parent', () => { let css = parse('') type(css.prev(), 'undefined') }) test('toJSON() cleans parents inside', () => { let rule = new Rule({ selector: 'a' }) rule.append({ prop: 'color', value: 'b' }) let json = rule.toJSON() as any type(json.parent, 'undefined') type(json.nodes[0].parent, 'undefined') is( JSON.stringify(rule), '{"raws":{},"selector":"a","type":"rule","nodes":[' + '{"raws":{},"prop":"color","value":"b","type":"decl"}' + '],"inputs":[]}' ) }) test('toJSON() converts custom properties', () => { let root = new Root() as any root._cache = [1] root._hack = { toJSON() { return 'hack' } } equal(root.toJSON(), { _cache: [1], _hack: 'hack', inputs: [], nodes: [], raws: {}, type: 'root' }) }) test('raw() has shortcut to stringifier', () => { let rule = new Rule({ selector: 'a' }) is(rule.raw('before'), '') }) test('root() returns root', () => { let css = parse('@page{a{color:black}}') let page = css.first as AtRule let a = page.first as Rule let color = a.first as Declaration equal(color.root(), css) }) test('root() returns parent of parents', () => { let rule = new Rule({ selector: 'a' }) rule.append({ prop: 'color', value: 'black' }) equal(rule.first?.root(), rule) }) test('root() returns self on root', () => { let rule = new Rule({ selector: 'a' }) equal(rule.root(), rule) }) test('root() returns root in document', () => { let css = new Document({ nodes: [parse('@page{a{color:black}}')] }) let root = css.first as Root let page = root.first as AtRule let a = page.first as Rule let color = a.first as Declaration equal(color.root(), root) }) test('root() on root in document returns same root', () => { let document = new Document() let root = new Root() document.append(root) equal(document.first?.root(), root) }) test('root() returns self on document', () => { let document = new Document() equal(document.root(), document) }) test('cleanRaws() cleans style recursivelly', () => { let css = parse('@page{a{color:black}}') css.cleanRaws() is(css.toString(), '@page {\n a {\n color: black\n }\n}') let page = css.first as AtRule let a = page.first as Rule let color = a.first as Declaration type(page.raws.before, 'undefined') type(color.raws.before, 'undefined') type(page.raws.between, 'undefined') type(color.raws.between, 'undefined') type(page.raws.after, 'undefined') }) test('cleanRaws() keeps between on request', () => { let css = parse('@page{a{color:black}}') css.cleanRaws(true) is(css.toString(), '@page{\n a{\n color:black\n }\n}') let page = css.first as AtRule let a = page.first as Rule let color = a.first as Declaration not.type(page.raws.between, 'undefined') not.type(color.raws.between, 'undefined') type(page.raws.before, 'undefined') type(color.raws.before, 'undefined') type(page.raws.after, 'undefined') }) test('positionInside() returns position when node starts mid-line', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.positionInside(6), { column: 12, line: 1, offset: 11 }) }) test('positionInside() returns position when before contains newline', () => { let css = parse('a {\n one: X}') let a = css.first as Rule let one = a.first as Declaration equal(one.positionInside(6), { column: 9, line: 2, offset: 12 }) }) test('positionInside() returns position when node contains newlines', () => { let css = parse('a {\n\tone: 1\n\t\tX\n3}') let a = css.first as Rule let one = a.first as Declaration equal(one.positionInside(10), { column: 4, line: 3, offset: 15 }) }) test('positionInside() returns position after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.positionInside(15), { column: 3, line: 3, offset: 15 }) equal(two.positionInside(1), { column: 3, line: 3, offset: 15 }) one.remove() equal(a.positionInside(15), { column: 3, line: 3, offset: 15 }) equal(two.positionInside(1), { column: 3, line: 3, offset: 15 }) }) test('positionInside() supports multi-root documents', () => { let root = parse('a {} b {}', { document: '' }) is(root.source?.input.css, 'a {} b {}') is(root.source?.input.document, '') let a = root.first as Rule // Offset the source location of `a` to mimic syntaxes like `postcss-html` a.source = { end: { column: 12, line: 1, offset: 12 }, input: a.source!.input, start: { column: 8, line: 1, offset: 7 } } equal(a.positionInside(0), { column: 8, line: 1, offset: 7 }) equal(a.positionInside(1), { column: 9, line: 1, offset: 8 }) }) test('positionBy() returns position', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) }) test('positionBy() returns position after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) one.remove() equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) }) test('positionBy() returns position', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) }) test('positionBy() returns position after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) one.remove() equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) }) test('positionBy() returns position for word', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.positionBy({ word: 'one' }), { column: 6, line: 1, offset: 5 }) equal(one.positionBy({ word: 'X' }), { column: 11, line: 1, offset: 10 }) equal(a.positionBy({ word: '}' }), { column: 14, line: 1, offset: 13 }) }) test('positionBy() returns position for word after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) one.remove() equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) }) test('positionBy() returns position for index', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.positionBy({ index: 1 }), { column: 7, line: 1, offset: 6 }) }) test('positionBy() returns position for index after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) one.remove() equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) }) test('positionBy() supports multi-root documents', () => { let root = parse('a {} b {}', { document: '' }) is(root.source?.input.css, 'a {} b {}') is(root.source?.input.document, '') let a = root.first as Rule // Offset the source location of `a` to mimic syntaxes like `postcss-html` a.source = { end: { column: 12, line: 1, offset: 12 }, input: a.source!.input, start: { column: 8, line: 1, offset: 7 } } // `offset` is present because the `0` index returns `source.start` equal(a.positionBy({ index: 0 }), { column: 8, line: 1, offset: 7 }) equal(a.positionBy({ index: 1 }), { column: 9, line: 1, offset: 8 }) equal(a.positionBy({ word: 'a' }), { column: 8, line: 1, offset: 7 }) }) test('rangeBy() returns range', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.rangeBy(), { end: { column: 12, line: 1, offset: 11 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range when offsets are missing', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration // @ts-expect-error Testing non-standard AST if (one.source?.start) delete one.source.start.offset // @ts-expect-error Testing non-standard AST if (one.source?.end) delete one.source.end.offset equal(one.rangeBy(), { end: { column: 12, line: 1, offset: 11 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range for empty object even after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.rangeBy(), { end: { column: 10, line: 3, offset: 22 }, start: { column: 1, line: 1, offset: 0 } }) equal(two.rangeBy(), { end: { column: 9, line: 3, offset: 21 }, start: { column: 2, line: 3, offset: 14 } }) one.remove() equal(a.rangeBy(), { end: { column: 10, line: 3, offset: 22 }, start: { column: 1, line: 1, offset: 0 } }) equal(two.rangeBy(), { end: { column: 9, line: 3, offset: 21 }, start: { column: 2, line: 3, offset: 14 } }) }) test('rangeBy() returns range', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.rangeBy(), { end: { column: 12, line: 1, offset: 11 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range when offsets are missing', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration // @ts-expect-error Testing non-standard AST if (one.source?.start) delete one.source.start.offset // @ts-expect-error Testing non-standard AST if (one.source?.end) delete one.source.end.offset equal(one.rangeBy(), { end: { column: 12, line: 1, offset: 11 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range for empty object even after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.rangeBy(), { end: { column: 10, line: 3, offset: 22 }, start: { column: 1, line: 1, offset: 0 } }) equal(two.rangeBy(), { end: { column: 9, line: 3, offset: 21 }, start: { column: 2, line: 3, offset: 14 } }) one.remove() equal(a.rangeBy(), { end: { column: 10, line: 3, offset: 22 }, start: { column: 1, line: 1, offset: 0 } }) equal(two.rangeBy(), { end: { column: 9, line: 3, offset: 21 }, start: { column: 2, line: 3, offset: 14 } }) }) test('rangeBy() returns range for word', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.rangeBy({ word: 'one' }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range for word when offsets are missing', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration // @ts-expect-error Testing non-standard AST if (one.source?.start) delete one.source.start.offset // @ts-expect-error Testing non-standard AST if (one.source?.end) delete one.source.end.offset equal(one.rangeBy({ word: 'one' }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 6, line: 1, offset: 5 } }) }) test('rangeBy() returns range for word even after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) equal(two.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) one.remove() equal(a.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) equal(two.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) }) test('rangeBy() returns range for word even after AST mutations when offsets are missing', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration // @ts-expect-error Testing non-standard AST if (a.source?.start) delete a.source.start.offset // @ts-expect-error Testing non-standard AST if (a.source?.end) delete a.source.end.offset // @ts-expect-error Testing non-standard AST if (two.source?.start) delete two.source.start.offset // @ts-expect-error Testing non-standard AST if (two.source?.end) delete two.source.end.offset equal(a.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) equal(two.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) one.remove() equal(a.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) equal(two.rangeBy({ word: 'two' }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 2, line: 3, offset: 14 } }) }) test('rangeBy() returns range for start and end', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal( one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 7, line: 1, offset: 6 } } ) }) test('rangeBy() returns range for start and end when offsets are missing', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration // @ts-expect-error Testing non-standard AST if (one.source?.start) delete one.source.start.offset // @ts-expect-error Testing non-standard AST if (one.source?.end) delete one.source.end.offset equal( one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 7, line: 1, offset: 6 } } ) }) test('rangeBy() returns range for start and end after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal( a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } } ) equal( two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } } ) one.remove() equal( a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } } ) equal( two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } } ) }) test('rangeBy() returns range for index and endIndex', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration equal(one.rangeBy({ endIndex: 3, index: 1 }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 7, line: 1, offset: 6 } }) }) test('rangeBy() returns range for index and endIndex when offsets are missing', () => { let css = parse('a { one: X }') let a = css.first as Rule let one = a.first as Declaration // @ts-expect-error Testing non-standard AST if (one.source?.start) delete one.source.start.offset // @ts-expect-error Testing non-standard AST if (one.source?.end) delete one.source.end.offset equal(one.rangeBy({ endIndex: 3, index: 1 }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 7, line: 1, offset: 6 } }) }) test('rangeBy() returns range for index and endIndex after AST mutations', () => { let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') let a = css.first as Rule let one = a.first as Declaration let two = one.next() as Declaration equal(a.rangeBy({ endIndex: 17, index: 15 }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } }) equal(two.rangeBy({ endIndex: 3, index: 1 }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } }) one.remove() equal(a.rangeBy({ endIndex: 17, index: 15 }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } }) equal(two.rangeBy({ endIndex: 3, index: 1 }), { end: { column: 5, line: 3, offset: 17 }, start: { column: 3, line: 3, offset: 15 } }) }) test('rangeBy() supports multi-root documents', () => { let root = parse('a {} b {}', { document: '' }) is(root.source?.input.css, 'a {} b {}') is(root.source?.input.document, '') let a = root.first as Rule // Offset the source location of `a` to mimic syntaxes like `postcss-html` a.source = { end: { column: 12, line: 1, offset: 12 }, input: a.source!.input, start: { column: 8, line: 1, offset: 7 } } equal(a.rangeBy({ endIndex: 1, index: 0 }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 8, line: 1, offset: 7 } }) equal(a.rangeBy({ word: 'a' }), { end: { column: 9, line: 1, offset: 8 }, start: { column: 8, line: 1, offset: 7 } }) }) test.run()