Skip to main content

README.md

# magic_string

[![Package Version](https://img.shields.io/hexpm/v/magic_string)](https://hex.pm/packages/magic_string)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/magic_string/)

Edit a string by byte offset and get a [source map](https://tc39.es/ecma426/) out the other side. A Gleam port of Rich Harris's [magic-string](https://github.com/Rich-Harris/magic-string).

It's useful when you have some source code, you know where the bits you care about are (because a parser told you), and you want to make surgical changes without rebuilding the whole thing from an AST. The source map is a byproduct of the edit log, so you don't have to think about it.

```sh
gleam add magic_string
```

```gleam
import magic_string as ms
import magic_string/codec

pub fn main() {
  let s = ms.new("const answer = 42;")

  // Byte offsets are half-open: [start, end). overwrite/remove return a
  // Result because they can conflict with earlier edits (see below).
  let assert Ok(s) = ms.overwrite(s, 6, 12, "x")
  let assert Ok(s) = ms.remove(s, 0, 6)
  let s = ms.append(s, " // edited")

  ms.to_string(s)
  // -> "x = 42; // edited"

  let map = ms.generate_map(s, "in.js", ms.default_map_options())
  codec.to_json(map)
  // -> {"version":3,"sources":["in.js"],"mappings":"...",...}
}
```

### Bundling multiple sources

```gleam
import magic_string as ms

let a = "export const a = 1;"
let b = "export const b = 2;"

let bundle =
  ms.bundle()
  |> ms.add_source("a.js", a, ms.new(a))
  |> ms.add_source("b.js", b, ms.new(b))

ms.bundle_to_string(bundle)
ms.bundle_generate_map(bundle, ms.default_map_options())
```

The map has `sources` and `sourcesContent` populated for every file you added, so a debugger can show the original code.

### API

The full API lives in the [hexdocs](https://hexdocs.pm/magic_string/). The short version:

- `new`, `overwrite`, `remove`, `append_left`, `append_right`, `prepend`, `append`, `to_string`, `generate_map` for single strings
- `bundle`, `add_source`, `bundle_to_string`, `bundle_generate_map` for stitching many together
- `magic_string/codec` has the Source Map v3 type, VLQ encoding, and `to_json` / `url_comment` if you need lower level access
- `magic_string/position_index` converts byte offsets to `(line, utf16_column)` pairs, which is what source maps want

### Notes

Offsets are UTF-8 byte offsets, not character indices. On the BEAM that's what `string.byte_size` and friends give you, and it's what most parsers report. The position index handles the conversion to UTF-16 columns for the source map output.

`overwrite`, `remove`, `append_left` and `append_right` return `Result(MagicString, EditError)`. They fail when the edit conflicts with one already recorded: two ranges overlapping (`Overlap`), an insert falling inside an existing range or a new range swallowing an existing insert (`SwallowedInsert`), or offsets outside the source (`OutOfBounds`, `InvertedRange`). The error names the offsets involved, so you can trace it back to the bad span. Adjacent edits that share a boundary but no bytes are fine. `prepend`, `append`, `to_string` and `generate_map` are infallible, since by the time you reach them every recorded edit is already known to compose.

In a bundler the offsets come straight from a parser's spans, so in practice you `result.try` through a chain of edits and surface the first conflict as a build error.

Also see: my project [Arc](https://github.com/alii/arc)!