끊어진 블록의 중단점 (Breakpoints on broken blocks)

테이블 헤더 및 푸터를 사용한 구현

여기에서 데모 프로젝트를 확인하세요(더 많은 주석이 있으며, 일부는 제거했습니다).

/// author: isuffix

// 기본 카운터 및 지그재그 함수
#let counter-family(id) = {
  let parent = counter(id)
  let parent-step() = parent.step()
  let get-child() = counter(id + str(parent.get().at(0)))
  return (parent-step, get-child)
}

// 재미있는 지그재그 선!
#let zig-zag(fill: black, rough-width: 6pt, height: 4pt, thick: 1pt, angle: 0deg) = {
  layout((size) => {
    // layout을 사용하여 크기를 얻고 수평 거리를 측정
    // 그 다음 수학을 사용하여 지그재그 당 너비 구함
    let count = int(calc.round(size.width / rough-width))
    // `h(-thick)`으로 결합하므로 추가 두께를 더해야 함
    let width = thick + (size.width - thick) / count
    // 지그(Zig)와 재그(Zag):
    let zig-and-zag = {
      let line-stroke = stroke(thickness: thick, cap: "round", paint: fill)
      let top-left = (thick/2, thick/2)
      let bottom-mid = (width/2, height - thick/2)
      let top-right = (width - thick/2, thick/2)
      let zig = line(stroke: line-stroke, start: top-left, end: bottom-mid)
      let zag = line(stroke: line-stroke, start: bottom-mid, end: top-right)
      box(place(zig) + place(zag), width: width, height: height, clip: true)
    }
    let zig-zags = ((zig-and-zag,) * count).join(h(-thick))
    rotate(zig-zags, angle)
  })
}

// ---- split-box 정의 ---- //

// 사용자 정의 가능한 split-box 테두리 옵션:
#let default-border = (
  // 시작 및 끝 선
  above: line(length: 100%),
  below: line(length: 100%),
  // 여러 페이지에 걸쳐 상자 사이에 넣을 선
  btwn-above: line(length: 100%, stroke: (dash:"dotted")),
  btwn-below: line(length: 100%, stroke: (dash:"dotted")),
  // 왼쪽/오른쪽 선
  // 이들은 *반드시* `grid.vline()`을 사용해야 하며, 그렇지 않으면 오류가 발생합니다.
  // 선을 제거하려면 `grid.vline(stroke: none)`으로 설정하세요.
  // rowspan으로 더 잘 구성할 수도 있겠지만, 귀찮네요.
  left: grid.vline(),
  right: grid.vline(),
)

// 여러 페이지/열에 걸치고
// 열 중단 위아래에 사용자 정의 테두리가 있는 상자 생성
#let split-box(
  // 테두리 딕셔너리 설정, 옵션은 위의 `default-border` 참조
  border: default-border,
  // 콘텐츠를 배치할 셀, `grid.cell`로 해석되어야 함
  cell: grid.cell.with(inset: 5pt),
  // 마지막 위치 인수(들)은 실제 콘텐츠입니다.
  // 추가 명명된 인수는 호출 시 기본 그리드로 전송됩니다.
  // fill, align 등에 유용합니다.
  ..args
) = {
  // 더 많은 정보는 `utils.typ` 참조.
  let (parent-step, get-child) = counter-family("split-box-unique-counter-string")
  parent-step() // 부모 카운터를 한 번 배치.
  // 헤더가 페이지에 배치될 때마다 추적.
  // 그런 다음 첫 번째 배치(헤더)인지 마지막(푸터)인지 확인.
  // 그렇지 않다면 테두리 선의 'between' 형태를 사용.
  let border-above = context {
    let header-count = get-child()
    header-count.step()
    context if header-count.get() == (1,) { border.above } else { border.btwn-above }
  }
  let border-below = context {
    let header-count = get-child()
    if header-count.get() == header-count.final() { border.below } else { border.btwn-below }
  }
  // 그리드 배치!
  grid(
    ..args.named(),
    columns: 3,
    border.left,
    grid.header(border-above , repeat: true),
    ..args.pos().map(cell),
    grid.footer(border-below, repeat: true),
    border.right,
  )
}

// ---- 예제 ---- //

#set page(width: 7.2in, height: 3in, columns: 6)

// 짜잔!
#split-box[
  #lorem(20)
]

// 재미있는 예제:

#let fun-border = (
  // 그라디언트!
  above: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow)),
  below: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow, angle: 180deg)),
  // 지그재그!
  btwn-above: move(dy: +2pt, zig-zag(fill: blue, angle: 3deg)),
  btwn-below: move(dy: -2pt, zig-zag(fill: orange, angle: 177deg)),
  left: grid.vline(stroke: (cap: "round", paint: purple)),
  right: grid.vline(stroke: (cap: "round", paint: purple)),
)

#split-box(border: fun-border)[
  #lorem(25)
]

// 그리고 좀 더 얌전한 친구들:

#split-box(border: (
  above: move(dy: -0.5pt, line(length: 100%)),
  below: move(dy: +0.5pt, line(length: 100%)),
  // 지그재그!
  btwn-above: move(dy: -1.1pt, zig-zag()),
  btwn-below: move(dy: +1.1pt, zig-zag(angle: 180deg)),
  left: grid.vline(stroke: (cap: "round")),
  right: grid.vline(stroke: (cap: "round")),
))[
  #lorem(10)
]

#split-box(
  border: (
    above: line(length: 100%, stroke: luma(50%)),
    below: line(length: 100%, stroke: luma(50%)),
    btwn-above: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    btwn-below: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    left: grid.vline(stroke: none),
    right: grid.vline(stroke: none),
  ),
  cell: grid.cell.with(inset: 5pt, fill: color.yellow.saturate(-85%))
)[
  #lorem(20)
]
Rendered image

헤더, 푸터 및 상태(stated)를 통한 구현

제한 사항: 단일 열 레이아웃과 한 번의 중단에서만 작동합니다.
#let countBoundaries(loc, fromHeader) = {
  let startSelector = selector(label("boundary-start"))
  let endSelector = selector(label("boundary-end"))

  if fromHeader {
    // 페이지 상단에서 카운트 다운
    startSelector = startSelector.after(loc)
    endSelector = endSelector.after(loc)
  } else {
    // 페이지 하단에서 카운트 업
    startSelector = startSelector.before(loc)
    endSelector = endSelector.before(loc)
  }

  let startMarkers = query(startSelector)
  let endMarkers = query(endSelector)
  let currentPage = loc.position().page

  let pageStartMarkers = startMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  let pageEndMarkers = endMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  (start: pageStartMarkers.len(), end: pageEndMarkers.len())
}

#set page(
  margin: 2em,
  // ... 다른 페이지 설정 ...
  header: context {
    let boundaryCount = countBoundaries(here(), true)

    if boundaryCount.end > boundaryCount.start {
      // 이 헤더를 여는 장식으로 꾸미기
      [Block break top: $-->$]
    }
  },
  footer: context {
    let boundaryCount = countBoundaries(here(), false)

    if boundaryCount.start > boundaryCount.end {
      // 이 푸터를 닫는 장식으로 꾸미기
      [Block break end: $<--$]
    }
  }
)

#let breakable-block(body) = block({
  [
    #metadata("boundary") <boundary-start>
  ]
  stack(
    // 분리 가능한 목록 콘텐츠가 여기에 들어감
    body
  )
  [
    #metadata("boundary") <boundary-end>
  ]
})

#set page(height: 10em)

#breakable-block[
    #([Something \ ]*10)
]
Rendered image
Rendered image