How do I best show change bars (revision bars)?

Objective / Question
I write technical reports. Our company requires that changed text be marked. The preferred way to do this is with a vertical bar to the side of any changed line, like so:
image

I’d like to be able to run something like git diff --color-words on a typst document to mark the changes, then use regex or show rules to replace the terminal color codes with Typst functions to add vertical bars next to any changed lines. (I’m aware manual cleanup would be necessary in some cases)

What’s the best way to implement this in Typst?

What I’ve tried
The drafting and pinit packages seem to have useful functions here (thanks to Nathan Jessurun and orangeX4!). I can use pinit’s pins to mark the extents of my change and edit the pinit-line function to draw the revbar (code below) but I haven’t been able to generalize this to encompass changes across pagebreaks. Additionally, my pinit approach currently requires uniquely named pins, which will be painful to deal with in any kind of regex/show rule solution.

#import "@preview/pinit:0.2.2":*

#set page("a5")

#let side-line(
  stroke: gray+0.5pt,
  start-dx: 0pt,  start-dy: 0pt,  end-dx: 0pt,  end-dy: 0pt,
  start, end,
) = {
  pinit(
    start,  
    end,
    callback: (start-pos, end-pos) => {
      absolute-place(
        line(
          stroke: stroke,
          start: (13.5cm, start-pos.y + start-dy - 12pt,),
          end: (13.5cm, end-pos.y + end-dy + 3pt,
          ),
        ),
      )
    },
  )
}

#lorem(12)
#pin("begin_change") #box(side-line("begin_change","end_change")) 
#lorem(4)
#pin("end_change")
#lorem(12)

I’ve experimented with adapting the pinit function to use a default end-of-page pin when it could not find the end-change pin on a page. However, this made the typst layout unstable when the change spanned a pagebreak.

Thanks in advance for any help you can offer!

Without showing you, I think it would be possible as long as:

  • you use the code block with syntax diff
  • you implement a word-diff algorithm (or get the results of one) comparing lines with + and the next line with -, or blocks of +/-.
  • you color the diff words and re-construct the structure of your text (this is the tricky part)

I highly do not recommend trying, except if you really want to … I cannot stop you from jumping into that rabbit hole. Otherwise there are tools like diffpdf that will do exactly what you want on different versions of your document.

Wise words - I’ll continue to try to avoid this solution :grinning:. I appreciate the recommendation on other tools. Unfortunately, I’m challenged on what software I can use thanks to bigcorp rules, and I need to mark actual changed text, but not reflowed text.

Having failed at avoiding the marking of changes using a diff of the typst source, I’ll document the rather terrible solution I’ve cobbled together. I’m indebted to the drafting author and contributors, but please don’t blame them for the desecration below.

Execution of a bad idea

The key concept is to have a margin-line function that draws a text-height margin bar, then spam this function so that it shows up on any changed line of text. Since my source files have short line of text lengths relative to published .pdfs, this usually works.

1. diff at word-level across the whole file.

git diff -U9999 --color-words --ignore-all-space newfile.typ oldfile.typ

This gives something like (I’ve replaced the ANSI control codes with tags for readability):

lorem ipsum <old-red>old text<reset><new-green>new text<reset>
<old-red>more old text<reset>
<new-green>more new text<reset>
incumbent text. Unchanged text<reset>

2. Regex replacement

Using a separate script, I do a bunch of replacements along the following lines

regex.substitute("<old-red>.*?<reset>","#margin-line()")
regex.substitute("<new-green>(.*?)<reset>","#margin-line()\g<1>#margin-line()")
regex.substitute("<reset>","")

to get:

lorem ipsum #margin-line()new text
#margin-line()
#margin-line()more new text#margin-line()
incumbent text. Unchanged text

(The regex could possibly be done in Typst as well, but I’m using what I know)

3. To Typst, finally

#import "@preview/drafting:0.2.2":*

#set page("a6")

#set-page-properties()

#let margin-line(stroke: gray + 1pt) = {
  context {
    let pos = here().position()
    let properties = margin-note-defaults.get()
    let (anchor-x, anchor-y) = (pos.x - properties.page-offset-x, pos.y)
    //place(dx: -2%, rect(height: 12pt, width: 104%, stroke: (left: stroke, right: stroke)))
    let dy=-12pt
    // Notes at the end of a line misreport their position. The placed box will wrap
    // onto the next line and invalidate the calculated distance.
    // A hacky fix is to manually offset to the next line.

    let end_of_line_detection_zone = 12pt
    let is-end-of-line = (
        calc.abs(anchor-x - properties.margin-left - properties.page-width - properties.page-offset-x) < end_of_line_detection_zone
      )
    if is-end-of-line {
      dy = dy + 12pt
    }
    //let properties = properties.named()
    let pct = _get-page-pct(properties)
    let dist-to-margin = 101 * pct - anchor-x + properties.margin-left
    let text-offset = 0.5em
    let right-width = properties.margin-right - 4 * pct
    box[
      //boxing prevents a forced linebreak
      #place(dx: dist-to-margin + 1 * pct, dy: dy)[
        #line(angle:90deg,length:14pt,stroke:stroke)]
    ]
  }
}

#lorem(12) #margin-line() new text 
#margin-line()
#margin-line()more new text#margin-line()
incumbent text. Unchanged text #lorem(5)

4. Success

image

Warnings and thanks!

Obviously, this method is extremely hacky. It requires that typset line lengths in the source file be considerably shorter than in the rendered pdf. And the dumb regex has no idea where it’s putting functions so it frequently breaks something.

At this point, I want to mention Visual diff of two Typst documents #3049. Give it an upvote or work on a PR if you’re able!

Thanks to all Typst-people for the powerful typesetter. Thanks especially to Nathan Jessurun for drafting! I’ll leave this unsolved for a few days so y’all can let me know your thoughts and if there’s a smarter way to get this done.

2 Likes