Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ Indicates if the string has been changed.

Prefixes each line of the string with `prefix`. If `prefix` is not supplied, the indentation will be guessed from the original content, falling back to a single tab character. Returns `this`.

Lines are counted in the generated string, so content added with `append*`/`prepend*` is indented too, and a line break inside that content starts a new line just like a line break in the original:

```js
const s = new MagicString('var a = 1;\nvar b = 2;')

s.prependRight(11, 'debugger;\n')
s.toString() // 'var a = 1;\ndebugger;\nvar b = 2;'

s.indent(' ')
s.toString() // ' var a = 1;\n debugger;\n var b = 2;'
```

The `options` argument can have an `exclude` property, which is an array of `[start, end]` character ranges. These ranges will be excluded from the indentation - useful for (e.g.) multiline strings.

### s.insertLeft( index, content )
Expand Down
43 changes: 30 additions & 13 deletions src/MagicString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,14 +365,23 @@ export default class MagicString {
}

let shouldIndentNextCharacter = options.indentStart !== false
const replacer = (match: string) => {
if (shouldIndentNextCharacter)
return `${resolvedIndentStr}${match}`
shouldIndentNextCharacter = true
return match

// Indents every line break inside `str`, plus the very first character when
// `str` starts a line in the generated output. Any match after offset 0
// necessarily follows a line break inside `str`, so it always gets indented.
const indentPiece = (str: string) => {
if (str === '')
return str

const indented = str.replace(pattern, (match: string, offset: number) =>
offset > 0 || shouldIndentNextCharacter ? `${resolvedIndentStr}${match}` : match)

shouldIndentNextCharacter = str[str.length - 1] === '\n'

return indented
}

this.intro = this.intro.replace(pattern, replacer)
this.intro = indentPiece(this.intro)

let charIndex = 0
let chunk = this.firstChunk
Expand All @@ -381,7 +390,10 @@ export default class MagicString {
shouldIndentNextCharacter = false

if (index === chunk!.start) {
chunk!.prependRight(resolvedIndentStr)
// `appendRight` rather than `prependRight`, so that the indent lands
// directly in front of the content rather than in front of an intro
// that has already been indented in its own right
chunk!.appendRight(resolvedIndentStr)
}
else {
this._splitChunk(chunk!, index)
Expand All @@ -393,13 +405,14 @@ export default class MagicString {
while (chunk) {
const end = chunk.end

// content added with `appendRight`/`prependRight` is emitted before the
// chunk itself, so it has to be indented before the chunk is walked
if (!isExcluded[chunk.start])
chunk.intro = indentPiece(chunk.intro)

if (chunk.edited) {
if (!isExcluded[charIndex]) {
chunk.content = chunk.content.replace(pattern, replacer)

if (chunk.content.length) {
shouldIndentNextCharacter = chunk.content[chunk.content.length - 1] === '\n'
}
chunk.content = indentPiece(chunk.content)
}
}
else if (options.exclude) {
Expand Down Expand Up @@ -446,11 +459,15 @@ export default class MagicString {
}
}

// ...and content added with `appendLeft`/`prependLeft` is emitted after it
if (!isExcluded[chunk.end - 1])
chunk.outro = indentPiece(chunk.outro)

charIndex = chunk.end
chunk = chunk.next
}

this.outro = this.outro.replace(pattern, replacer)
this.outro = indentPiece(this.outro)

return this
}
Expand Down
114 changes: 114 additions & 0 deletions test/MagicString.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,120 @@ describe('magicString', () => {
assert.equal(s.toString(), '\tclass Foo extends Baz {}')
})

it('should indent content added with appendRight/prependRight', () => {
const s = new MagicString('a\nb\nc')

s.appendRight(2, 'Q\n')
assert.equal(s.toString(), 'a\nQ\nb\nc')

s.indent('>')
assert.equal(s.toString(), '>a\n>Q\n>b\n>c')
})

it('should indent content added with appendLeft/prependLeft', () => {
const s = new MagicString('a\nb\nc')

s.appendLeft(1, '\nQ')
assert.equal(s.toString(), 'a\nQ\nb\nc')

s.indent('>')
assert.equal(s.toString(), '>a\n>Q\n>b\n>c')
})

it('should indent a line that starts inside inserted content', () => {
const s = new MagicString('a\nb\nc')

s.appendLeft(2, 'X')
assert.equal(s.toString(), 'a\nXb\nc')

// the indent belongs in front of the insert, not between it and `b`
s.indent('>')
assert.equal(s.toString(), '>a\n>Xb\n>c')
})

it('should indent original content that follows a multiline insert', () => {
const s = new MagicString('a\nb\nc')

s.prependRight(2, 'one\ntwo\n')
assert.equal(s.toString(), 'a\none\ntwo\nb\nc')

s.indent('>')
assert.equal(s.toString(), '>a\n>one\n>two\n>b\n>c')
})

it('should not indent content that continues the current line', () => {
const s = new MagicString('a\nb\nc')

s.appendRight(2, 'X')
assert.equal(s.toString(), 'a\nXb\nc')

s.indent('>')
assert.equal(s.toString(), '>a\n>Xb\n>c')
})

it('should indent lines that start inside the outro', () => {
const s = new MagicString('a\nb\nc')

s.append('\nZ')
assert.equal(s.toString(), 'a\nb\nc\nZ')

s.indent('>')
assert.equal(s.toString(), '>a\n>b\n>c\n>Z')
})

it('should indent every line of a wrapped module', () => {
const s = new MagicString('var a = 1;\nvar b = 2;')

s.prepend('(function () {\n')
s.prependRight(11, 'debugger;\n')
s.append('\n}());')
assert.equal(
s.toString(),
'(function () {\nvar a = 1;\ndebugger;\nvar b = 2;\n}());',
)

s.indent(' ')
assert.equal(
s.toString(),
' (function () {\n var a = 1;\n debugger;\n var b = 2;\n }());',
)
})

it('should indent every line of the generated string', () => {
const original = 'const a = 1;\nconst b = 2;\n\nexport { a, b };\n'

const build = (s: MagicString) => {
s.prepend('// header\n')
s.appendLeft(12, '\nconst inserted = 3;')
s.overwrite(13, 25, 'const b = 20;\nconst c = 30;')
s.append('// footer\n')
}

const generated = new MagicString(original)
build(generated)

const indented = new MagicString(original)
build(indented)
indented.indent(' ')

assert.equal(
indented.toString(),
generated
.toString()
.split('\n')
.map(line => (line === '' ? line : ` ${line}`))
.join('\n'),
)
})

it('should respect indentStart across inserted content', () => {
const s = new MagicString('a\nb')

s.prependRight(0, 'X')
s.indent('>', { indentStart: false })
assert.equal(s.toString(), 'Xa\n>b')
})

it('should return this', () => {
const s = new MagicString('abcdefghijkl')
assert.strictEqual(s.indent(), s)
Expand Down
Loading