diff --git a/packages/preview/bytefield/0.0.8/LICENSE b/packages/preview/bytefield/0.0.8/LICENSE new file mode 100644 index 0000000000..17c40758ec --- /dev/null +++ b/packages/preview/bytefield/0.0.8/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 jomaway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/bytefield/0.0.8/README.md b/packages/preview/bytefield/0.0.8/README.md new file mode 100644 index 0000000000..512f1f6a32 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/README.md @@ -0,0 +1,78 @@ +# typst-bytefield + +A simple way to create network protocol headers, memory maps, register definitions and more in typst. + +⚠️ Warning. As this package is still in an early stage, things might break with the next version. + +ℹ️ If you find a bug or a feature which is missing, please open an issue and/or send an PR. + +## Example + +![random colored bytefield example](https://github.com/jomaway/typst-bytefield/blob/a7c649a4d0e4ab634e4afa6d170e000b023e4313/docs/bytefield_example.png) + +```typst +#import "@preview/bytefield:0.0.8": * + +#bytefield( +// Config the header +bitheader( +"bytes", +// adds every multiple of 8 to the header. +0, [start], // number with label +5, +// number without label +12, [#text(14pt, fill: red, "test")], +23, [end_test], +24, [start_break], +36, [Fix], // will not be shown +angle: -50deg, // angle (default: -60deg) +text-size: 8pt, // length (default: global header_font_size or 9pt) +), +// Add data fields (bit, bits, byte, bytes) and notes +// A note always aligns on the same row as the start of the next data field. +note(left)[#text(16pt, fill: blue, "Testing")], +bytes(3,fill: red.lighten(30%))[Test], +note(right)[#set text(9pt); #sym.arrow.l This field \ breaks into 2 rows.], +bytes(2)[Break], +note(left)[#set text(9pt); and continues \ here #sym.arrow], +bits(24,fill: green.lighten(30%))[Fill], +group(right,3)[spanning 3 rows], +bytes(12)[#set text(20pt); *Multi* Row], +note(left, bracket: true)[Flags], +bits(4)[#text(8pt)[reserved]], +flag[#text(8pt)[SYN]], +flag(fill: orange.lighten(60%))[#text(8pt)[ACK]], +flag[#text(8pt)[BOB]], +bits(25, fill: purple.lighten(60%))[Padding], +// padding(fill: purple.lighten(40%))[Padding], +bytes(2)[Next], +bytes(8, fill: yellow.lighten(60%))[Multi break], +note(right)[#emoji.checkmark Finish], +bytes(2)[_End_], +) +``` + + +## Usage + +To use this library through the Typst package manager import bytefield with `#import "@preview/bytefield:0.0.8": *` at the top of your file. + +The package contains some of the most common network protocol headers which are available under: `common.ipv4`, `common.ipv6`, `common.icmp`, `common.icmpv6`, `common.dns`, `common.tcp`, `common.udp`. + +## Features + +Here is a unsorted list of features which is possible right now. + +- Adding fields with `bit`, `bits`, `byte` or `bytes` function. + - Fields can be colored + - Multirow and breaking fields are supported. +- Adding notes to the left or right with `note` or `group` function. +- Config the header with the `bitheader` function. !Only one header per bytefield is processed currently. + - Show numbers + - Show numbers and labels + - Show only labels +- Change the bit order in the header with `msb:left` or `msb:right` (default) + + +See [example.typ](https://github.com/jomaway/typst-bytefield/blob/a7c649a4d0e4ab634e4afa6d170e000b023e4313/examples/example.typ) for more information. + diff --git a/packages/preview/bytefield/0.0.8/bytefield.typ b/packages/preview/bytefield/0.0.8/bytefield.typ new file mode 100644 index 0000000000..b4bb039f12 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/bytefield.typ @@ -0,0 +1,7 @@ +// Bytefield - generate protocol headers and more + +// pull in the user facing api +#import "lib/api.typ": bytefield, bit, bits, byte, bytes, flag, group, note, bitheader, section +#import "lib/states.typ": bf-config +// import common network protocols +#import "common.typ"; diff --git a/packages/preview/bytefield/0.0.8/common.typ b/packages/preview/bytefield/0.0.8/common.typ new file mode 100644 index 0000000000..02b7159440 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/common.typ @@ -0,0 +1,93 @@ +#import "lib/api.typ": * + +// Common network protocols +#let eth = bytefield( + bpr: 8 * 6, + bitheader("bytes"), + bytes(6)[Destination MAC], + bytes(6)[Source MAC], + bytes(2)[Ethertype], + bytes(42)[Payload (42-1500)], + bytes(4)[CRC], +) + +#let ipv6_frag_ext_h = bytefield( + bitheader("bytes"), + bits(8)[Next Header], + bits(8)[Reserved], + bits(13)[Fragment offset], + bits(2)[res], + flag[M], + bits(4 * 8)[Identification], +) + +#let ipv4 = bytefield( + bitheader("bytes"), + bits(4)[Version], bits(4)[IHL], bytes(1)[TOS], bytes(2)[Total Length], + bytes(2)[Identification], bits(3)[Flags], bits(13)[Fragment Offset], + bytes(1)[TTL], bytes(1)[Protocol], bytes(2)[Header Checksum], + bytes(4)[Source Address], + bytes(4)[Destination Address], + bytes(3)[Options], bytes(1)[Padding] +) + +#let ipv6 = bytefield( + bitheader("bytes"), + bits(4)[Version], bytes(1)[Traffic Class], bits(20)[Flowlabel], + bytes(2)[Payload Length], bytes(1)[Next Header], bytes(1)[Hop Limit], + bytes(int(128/8))[Source Address], + bytes(int(128/8))[Destination Address], +) + +#let icmp = bytefield( + bitheader("bytes"), + byte[Type], byte[Code], bytes(2)[Checksum], + bytes(2)[Identifier], bytes(2)[Sequence Number], + bits(auto)[Optional Data] +) + +#let icmpv6 = bytefield( + bitheader("bytes"), + byte[Type], byte[Code], bytes(2)[Checksum], + bits(auto)[Internet Header + 64 bits of Original Data Datagram] +) + +#let dns = bytefield( + bitheader("bytes"), + bytes(2)[Identification], bytes(2)[Flags], + bytes(2)[Number of Questions], bytes(2)[Number of answer RRs], + bytes(2)[Number of authority RRs], bytes(2)[Number of additional RRs], + bytes(8)[Questions], + bytes(8)[Answers (variable number of resource records)], + bytes(8)[Authority (variable number of resource records)], + bytes(8)[Additional information (variable number of resource records)], +) + +#let tcp = bytefield( + bitheader("bytes"), + bytes(2)[Source Port], bytes(2)[Destination Port], + bytes(4)[Sequence Number], + bytes(4)[Acknowledgment Number], + bits(4)[Data Offset],bits(6)[Reserved], bits(6)[Flags], bytes(2)[Window], + bytes(2)[Checksum], bytes(2)[Urgent Pointer], + bytes(3)[Options], byte[Padding], + bits(auto)[...DATA...] +) + +#let tcp_detailed = bytefield( + bitheader("bytes"), + bytes(2)[Source Port], bytes(2)[ Destination Port], + bytes(4)[Sequence Number], + bytes(4)[Acknowledgment Number], + bits(4)[Data Offset],bits(6)[Reserved], flag("URG"), flag("ACK"), flag("PSH"), flag("RST"), flag("SYN"), flag("FIN"), bytes(2)[Window], + bytes(2)[Checksum], bytes(2)[Urgent Pointer], + bytes(3)[Options], byte[Padding], + bits(auto)[...DATA...] +) + +#let udp = bytefield( + bitheader("bytes"), + bytes(2)[Source Port], bytes(2)[ Destination Port], + bytes(2)[Length], bytes(2)[Checksum], + bits(auto)[...DATA...] +) diff --git a/packages/preview/bytefield/0.0.8/lib/api.typ b/packages/preview/bytefield/0.0.8/lib/api.typ new file mode 100644 index 0000000000..65990f79c1 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/api.typ @@ -0,0 +1,250 @@ +#import "gen.typ": * +#import "types.typ": * + +/// Create a new bytefield. +/// +/// *Example:* See @chap:use-cases +/// +/// +/// - bpr (int): Number of bits which are shown per row. +/// - msb (left, right): position of the msb. +/// - pre (auto, int , relative , fraction , array): This is specifies the columns for annotations on the *left* side of the bytefield +/// - post (auto, int , relative , fraction , array): This is specifies the columns for annotations on the *right* side of the bytefield +/// +/// - ..fields (bf-field): arbitrary number of data fields, annotations and headers which build the bytefield. +/// -> bytefield +#let bytefield( + bpr: 32, + msb: right, + rows: auto, + pre: auto, + post: auto, + ..fields +) = { + + let args = ( + bpr: bpr, + msb: msb, + rows: rows, + side: (left-cols: pre, right-cols: post) + ) + + let meta = generate-meta(fields.pos(), args) + let fields = finalize-fields(fields.pos(), meta) + let cells = generate-cells(meta, fields) + let table = generate-table(meta, cells) + return table +} + +/// Base for `bit`, `bits`, `byte`, `bytes`, `flag` field functions +/// +/// #emoji.warning This is just a base function which is used by the other functions and should *not* be called directly. +/// +/// - size (int): The size of the field in bits. +/// - fill (color): The background color for the field. +/// - body (content): The label which is displayed inside the field. +#let _field(size, fill: none, stroke: auto, body) = { + if size != auto { assert.eq(type(size), int, message: strfmt("expected size to be an integer or auto, found {}", type(size))) } + if fill != none { assert(type(fill) in (color, gradient, tiling), message: strfmt("expected fill to be an color or gradient, found {}.", type(fill)))} + let _stroke = if (stroke == auto) { (:) } else { (rest: stroke) } + data-field(none, size, none, none, body, format: (fill: fill, stroke: _stroke)) +} + +/// Add a field of the size of *one bit* to the bytefield +/// +/// Basically just a wrapper for @@_field() +/// +/// - ..args (arguments): All arguments which are accepted by `_field` +/// +#let bit(..args) = _field(1, ..args) + +/// Add a field of a given size of bits to the bytefield +/// +/// Basically just a wrapper for @@_field() +/// +/// - len (int): Size of the field in bits +/// - ..args (arguments): All arguments which are accepted by `_field` +/// +#let bits(len, ..args) = _field(len, ..args) + +/// Add a field of the size of one byte to the bytefield +/// +/// Basically just a wrapper for @@_field() +/// +/// - ..args (arguments): All arguments which are accepted by `_field` +/// +#let byte(..args) = _field(8, ..args) + +/// Add a field of the size of multiple bytes to the bytefield +/// +/// Basically just a wrapper for @@_field() +/// +/// - len (int): Size of the field in bytes +/// - ..args (arguments): All arguments which are accepted by `_field` +/// +#let bytes(len, ..args) = _field(len * 8, ..args) + +/// Add a flag to the bytefield. +/// +/// Basically just a wrapper for @@_field() +/// +/// - text (content): The label of the flag which is rotated by `270deg` +/// - ..args (arguments): All arguments which are accepted by `_field` +/// +#let flag(text,..args) = _field(1,align(center,rotate(270deg,text)),..args) + +/// Create a annotation +/// +/// The note is always shown in the same row as the next data field which is specified. +/// +/// - side (left, right): Where the annotation should be displayed +/// - row (int, auto): Define a row where the note should be displayed. +/// - rowspan (int): Defines if the cell is spanned over multiple rows. +/// - level (int): Defines the nesting level of the note. +/// - inset (length): Inset of the the annotation cell. +/// - bracket (bool): Defines if a bracket will be shown for this note. +/// - content (content): The content of the note. +#let note( + side, + row: auto, + rowspan:1, + level:0, + inset: 5pt, + bracket: false, + content, +) = { + assert(side in (left,right), message: strfmt("expected side to be left or right, found {}", side)); + assert.eq(type(bracket), bool, message: strfmt("expected bracket to be of type bool, found {}", type(bracket))); + assert.eq(type(rowspan), int, message: strfmt("expected rowspan to be a integer, found {}.", type(rowspan))); + assert.eq(type(level), int, message: strfmt("expected level to be a integer, found {}.", type(level))); + assert(type(inset) in (length, dictionary), message: strfmt("expected inset to be a length or dictionary, found {}.", type(inset))); + + + let _align = none + let _first = none + let _second = none + + if (side == left) { + _align = right + _first = content + _second = layout(size => {$ stretch(\{,size:size.height) $}) + } else { + _align = left + _first = layout(size => {$stretch(\}, size: size.height) $}) + _second = content + } + + let body = if (bracket == false) { content } else { + set math.equation(block: false, numbering: none) + grid( + columns:2, + gutter: inset, + _first, + _second + ) + } + + let format = ( + inset: if (bracket == false) { inset } else { (x:2pt, y:1pt) }, + align:_align+horizon, + ) + + return note-field(none, side, level:level, body, format: format, row: row, rowspan: rowspan) +} + +/// Shows a note with a bracket and spans over multiple rows. +/// +/// Basically just a shortcut for the `note` field with the argument `bracket:true` by default. +/// +#let group(side, rowspan, level:0, bracket:true, content) = { + note(side, level:level, rowspan:rowspan, bracket:bracket, content) +} + +/// Shows a special note with a start_addr (top aligned) and end_addr (bottom aligned) on the left of the associated row. +/// +/// #emoji.warning *experimental:* This will probably change in a future version. +/// +/// - start-addr (string, content): The start address will be top aligned +/// - end-addr (string, content): The end address will be bottom aligned +#let section(start-addr, end-addr, span: 1) = { + note( + left, + rowspan: span, + inset: (x:5pt, y:2pt), + box(height:100%, [ + #set text(0.8em, font: "Noto Mono", weight: 100) + #align(top + end)[#start-addr] + #align(bottom + end)[#end-addr] + ])) +} + +/// Config the header on top of the bytefield +/// +/// By default no header is shown. +/// +/// - range (array): Specify the range of number which are displayed on the header. Format: `(start, end)` +/// - autofill (string): Specify on of the following options and let bytefield calculate the numbers for you. +/// - `"bytes"` shows each multiple of 8 and the last number of the row. +/// - `"all"` shows all numbers. +/// - `"bounds"` shows a number for every start and end bit of a field. +/// - `"offsets"` shows a number for every start bit of a field. +/// - numbers (auto, none): if none is specified no numbers will be shown on the header. This is useful to show only labels. +/// - marker (bool, array): defines if marker lines are shown under a label. +/// - angle (angle): The rotation angle of the label. +/// - fill (color): The background of the header. +/// - stroke (color): The border stroke of the header. +/// - text-size(length): The text-size for the header labels and numbers. +/// - ..args (int, content): The numbers and labels which should be shown on the header. +/// The number will only be shown if it is inside the range. +/// If a `content` value follows a `int` value it will be interpreted as label for this number. +/// For more information see the _manual_. +#let bitheader( + range: (auto,auto), + autofill: auto, + numbers: auto, + marker: true, + fill: auto, + stroke: auto, + text-size: auto, + angle: -60deg, + ..args +) = { + // assertions + if (autofill != auto) { assert.eq(type(autofill), str, message: strfmt("expected autofill to be a string, found {}", type(autofill)))} + + let _numbers = () + let labels = (:) + let last = 0 + let step = 1 + for arg in args.pos() { + if type(arg) == int { + if (arg >= 0) { + _numbers.push(arg) + } + last = calc.abs(arg) + } else if type(arg) == str { + autofill = arg + } else if type(arg) == content { + labels.insert(str(last),arg) + last += 1 + } + if numbers != none { numbers = _numbers } + } + + let format = ( + angle: angle, + text-size: text-size, + marker: marker, + fill: fill, + stroke: stroke, + ) + + return header-field( + start: range.at(0, default: auto), // todo. fix this + end: range.at(1, default: auto), // todo. fix this + autofill: autofill, + numbers: numbers, + labels: labels, + ..format + ) +} diff --git a/packages/preview/bytefield/0.0.8/lib/asserts.typ b/packages/preview/bytefield/0.0.8/lib/asserts.typ new file mode 100644 index 0000000000..a4625adff4 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/asserts.typ @@ -0,0 +1,58 @@ +#import "@preview/oxifmt:1.0.0": strfmt + + +#let assert-dict(arg, var_name) = { + assert.eq(type(arg),dictionary, message: strfmt("expected {} to be a dictionary, found {}",var_name, type(arg))); +} + + +/// fail if field is not a bf-field +#let assert-bf-field(field) = { + assert-dict(field, "field") + let bf-type = field.at("bf-type", default: none) + assert.eq(bf-type, "bf-field", message: strfmt("expected bf-type of 'bf-field', found {}",bf-type)); + let field-type = field.at("field-type", default: none) + assert.ne(field-type, none, message: strfmt("could not find field-type at bf-field {}", field)); +} + +/// fail if field is not a bf-field of type data-field +#let assert-data-field(field) = { + assert-bf-field(field); + let field-type = field.at("field-type", default: none) + assert.eq(field-type, "data-field", message: strfmt("expected field-type == data-field, found {}",field-type)) + let size = field.data.size; + assert(type(size) == int, message: strfmt("expected integer for parameter size, found {} ", type(size))) +} + +/// fail if field is not a bf-field of type data-field +#let assert-note-field(field) = { + assert-bf-field(field); + // Check for correct field-type + let field-type = field.at("field-type", default: none) + assert.eq(field-type, "note-field", message: strfmt("expected field-type == note-field, found {}",field-type)) + // Check if it has a valid anchor id + let row = field.data.row + assert(type(row) == int, message: strfmt("expected integer for parameter anchor, found {} ", type(row))) + + // Check side + assert(field.data.side == left or field.data.side == right, message: strfmt("expected left or right for the notes side argument.")) +} + +/// fail if it does not match +#let assert-header-field(field) = { + assert-bf-field(field); +} + +/// fail if cell is not a bf-cell +#let assert-bf-cell(cell) = { + assert-dict(cell, "cell"); + // Check bf-type + let bf-type = cell.at("bf-type", default: none) + assert.eq(bf-type, "bf-cell", message: strfmt("expected bf-type of 'bf-cell', found {}",bf-type)); + // Check cell-type is not none + let cell-type = cell.at("cell-type", default: none) + assert.ne(cell-type, none, message: strfmt("could not find cell-type at bf-cell {}", cell)); +} + + + diff --git a/packages/preview/bytefield/0.0.8/lib/gen.typ b/packages/preview/bytefield/0.0.8/lib/gen.typ new file mode 100644 index 0000000000..eee73f095c --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/gen.typ @@ -0,0 +1,431 @@ +#import "types.typ": * +#import "utils.typ": * +#import "asserts.typ": * +#import "states.typ": * + +#let assert-level-cols(levels, cols, side) = { + if (cols != auto) { + cols = if (int == type(cols)) { cols } else if (array == type(cols)) { cols.len() } else { panic(strfmt("expected {} argument to be auto, int or array, found {}",if (side == "left") {"pre"} else {"post"} ,type(cols) )) } + + assert(cols >= levels, message: strfmt("max notes level on the {} is {}, but found only {} cols.",side, levels, cols)) + } +} + +/// generate metadata which is needed later on +#let generate-meta(fields, args) = { + // collect metadata into an dictionary + let bh = fields.find(f => is-header-field(f)) + // check level indentation for annotations + let (pre-levels, post-levels) = get-max-annotation-levels(fields.filter(f => is-note-field(f) )) + assert-level-cols(pre-levels, args.side.left-cols, "left") + assert-level-cols(post-levels, args.side.right-cols, "right") + // check if msb value is valid + assert(args.msb in (left,right), message: strfmt("expected left or right for msb, found {}", args.msb)) + let meta = ( + // the total size in bits of all data-fields inside the bytefield. // todo: check if this is calculated correct if msb:left is selected. + size: fields.filter(f => is-data-field(f) ).map(f => if (f.data.size == auto) { args.bpr /* fix: this is not consistent with how auto is handled in generate_fields */ } else { f.data.size } ).sum(), + // the position of the msb (left | right), default: right + msb: args.msb, + // number of cols for each grid. + cols: ( + pre: pre-levels, + main: args.bpr, + post: post-levels, + ), + // contains the height of the rows. + rows: ( + header: auto, // !unused + main: args.rows, // default: auto + ), + // contains information about the header + header: if (bh == none) { none } else { + ( + fill: bh.data.format.at("fill", default: none), + stroke: bh.data.format.at("stroke", default: none), + ) + }, + // stores the cols arguments for annotations + side: ( + left: ( + cols: if (args.side.left-cols == auto) { (auto,)*pre-levels } else { args.side.left-cols }, + ), + right: ( + cols: if (args.side.right-cols == auto) { (auto,)*post-levels } else { args.side.right-cols }, + ) + ) + ) + return meta; +} + +/// helper to calc number values from autofill string arguments +#let get-header-autofill-values(autofill, fields, meta) = { + if (autofill == "bounds") { + return fields.filter(f => is-data-field(f)).map(f => if f.data.range.start == f.data.range.end { (f.data.range.start,) } else {(f.data.range.start, f.data.range.end)}).flatten() + } else if (autofill == "all") { + return range(meta.size) + } else if (autofill == "offsets") { + let _fields = fields.filter(f => is-data-field(f)).map(f => f.data.range.start).flatten() + _fields.push(meta.cols.main -1) + _fields.push(meta.size -1) + return _fields.dedup() + } else if (autofill == "bytes") { + let _fields = range(meta.size, step: 8) + _fields.push(meta.cols.main -1) + _fields.push(meta.size -1) + return _fields.dedup() + } else { + () + } +} + +/// Index all fields +#let index-fields(fields) = { + fields.enumerate().map(((idx, f)) => { + assert-bf-field(f) + dict-insert-and-return(f, "field-index", idx) + }) +} + +/// indexes all fields and add some additional field data +#let finalize-fields(fields, meta) = { + + // This part must be changed if the user low level api changes. + let _fields = index-fields(fields) + + // Define some variables + let bpr = meta.cols.main + let range_idx = 0; + let fields = (); + + // data fields - important! must be processed first. + let data_fields = _fields.filter(f => is-data-field(f)) + if (meta.msb == left ) { data_fields = data_fields.rev() } + for f in data_fields { + let size = if (f.data.size == auto) { bpr - calc.rem(range_idx, bpr) } else { f.data.size } + let start = range_idx; + range_idx += size; + let end = range_idx - 1; + fields.push(data-field(f.field-index, size, start, end, f.data.label, format: f.data.format)) + } + + // note fields + for f in _fields.filter(f => is-note-field(f)) { + let row = if (f.data.row == auto) { + let anchor = get-index-of-next-data-field(f.field-index, _fields) + let anchor_field = fields.find(f => f.field-index == anchor) + int( if (meta.msb == left) { (meta.size - anchor_field.data.range.end)/bpr } else { anchor_field.data.range.start/bpr }) + } else { + f.data.row + } + let data = dict-insert-and-return(f.data, "row", row) + let field = dict-insert-and-return(f, "data", data) + assert-note-field(field); + fields.push(field) + } + + + // header fields -- needs data fields already processed !! + for f in _fields.filter(f => is-header-field(f)) { + let autofill_values = get-header-autofill-values(f.data.autofill, fields, meta); + let numbers = if f.data.numbers == none { () } else { f.data.numbers + autofill_values } + let labels = f.data.at("labels", default: (:)) + fields.push(header-field( + start: if f.data.range.start == auto { if meta.msb == right { 0 } else { meta.size - bpr } } else { assert.eq(type(f.data.range.start),int); f.data.range.start }, + end: if f.data.range.end == auto { if meta.msb == right { bpr } else { meta.size } } else { assert.eq(type(f.data.range.end),int); f.data.range.end }, + numbers: numbers, + labels: labels, + ..f.data.format, + )) + break // workaround to only allow one bitheader till multiple bitheaders are supported. + } + + return fields +} + +/// generate data cells from data-fields +#let generate-data-cells(fields, meta) = { + let data_fields = fields.filter(f => is-data-field(f)) + if (meta.msb == left ) { data_fields = data_fields.rev() } + data_fields = data_fields + let bpr = meta.cols.main; + + let _cells = (); + let idx = 0; + + for field in data_fields { + assert-data-field(field) + let len = field.data.size + + let slice_idx = 0; + let should_span = is-multirow(field, bpr) + let current_offset = calc.rem(idx, bpr) + + while len > 0 { + let rem_space = bpr - calc.rem(idx, bpr); + let cell_size = calc.min(len, rem_space); + + + let _stroke = field.data.format.at("stroke", default: auto) + + if (slice_idx == 0 and (len - cell_size) > bpr and not should_span) { + _stroke.insert("bottom", 0pt) + } else if (slice_idx > 0 and _cells.last().position.at("x") == 0){ + _stroke.insert("top", 0pt) + } else if (slice_idx > 0 and (len - cell_size) > 0) { + _stroke = (:) + } + + let cell_index = (field.field-index, slice_idx) + let x_pos = calc.rem(idx,bpr) + let y_pos = int(idx/bpr) + + let cell_format = (: + ..field.data.format, + stroke: _stroke, + ) + + // adjust label for breaking fields. + let middle = int(field.data.size / 2 / bpr) // roughly calc the row where the label should be displayed + let label = if (should_span or middle == slice_idx) { field.data.label } else if (slice_idx < 2 and len < bpr) { "..." } else {none} + + // prepare for next cell + let tmp_size = if should_span {field.data.size} else {cell_size} + idx += tmp_size; + len -= tmp_size; + slice_idx += 1; + + // add bf-cell to _cells + _cells.push( + //type, grid, x, y, colspan:1, rowspan:1, label, idx ,format: auto + bf-cell("data-cell", + x: x_pos, + y: y_pos, + colspan: cell_size, + rowspan: if(should_span) { int(field.data.size/bpr)} else {1}, + label: label, + cell-index: cell_index, + format: cell_format + ) + ) + } + } + return _cells +} + +/// generate note cells from note-fields +#let generate-note-cells(fields, meta) = { + let note_fields = fields.filter(f => is-note-field(f)) + let _cells = () + let bpr = meta.cols.main + + for field in note_fields { + assert-note-field(field) + let side = field.data.side + let level = field.data.level + + _cells.push( + bf-cell("note-cell", + cell-index: (field.field-index, 0), + grid: side, + x: if (side == left) { + meta.cols.pre - level - 1 + } else { + level + }, + y: int(field.data.row), + rowspan: field.data.rowspan, + label: field.data.label, + format: field.data.format, + ) + ) + } + return _cells +} + +/// generate header cells from header-fields +#let generate-header-cells(fields, meta) = { + let header_fields = fields.filter(f => is-header-field(f)) + let bpr = meta.cols.main + + let _cells = () + + for header in header_fields { + assert-header-field(header) + let nums = header.data.at("numbers", default: ()) + header.data.at("labels").keys().map(k => int(k)) + let cell = nums.filter(num => num >= header.data.range.start and num < header.data.range.end).dedup().map(num =>{ + + // extract the label from the header field. + let label = header.data.labels.at(str(num), default: "") + // check if the number should be shown on this cell. + let show_number = num in header.data.numbers + // calculate the x position inside the grid depending on the msb + let x_pos = if (meta.msb == left) { (bpr - 1) - calc.rem(num,bpr) } else { calc.rem(num, bpr) } + + // check if a marker should be used. + let marker = header.data.format.at("marker", default: true) // false + if (type(marker) == array) { + assert(marker.len() == 2, message: strfmt("expected a array of length two, found array with length {}", marker.len())); + if (show_number) { + marker = marker.at(0, default: true) + } else { + marker = marker.at(1, default: true) + } + } + assert(type(marker) == bool, message: strfmt("expects marker to be a bool, found {}", type(marker))); + + bf-cell("header-cell", + cell-index: (header.field-index, num), + grid: top, + x: x_pos, + y: 0, + label: ( + num: str(num), + text: label, + ), + format: ( + // Defines if the number should be shown or ommited + number: show_number, + // Defines the angle of the labels + angle: header.data.format.at("angle", default: -60deg), + // Defines the text-size for both numbers and labels. + text-size: header.data.format.at("text-size",default: auto), + // Defines if a marker should be shown + marker: marker, + // Defines the alignment + align: header.data.format.at("align", default: center + bottom), + // Defines the inset + inset: header.data.format.at("inset", default: (x: 0pt, y: 4pt)), + ) + ) + }) + + _cells.push(cell) + + } + + return _cells +} + +/// generates cells from fields +#let generate-cells(meta, fields) = { + // data + let data_cells = generate-data-cells(fields, meta); + // notes + let note_cells = generate-note-cells(fields, meta); + // header + let header_cells = generate-header-cells(fields, meta); + + return (header_cells, data_cells, note_cells).flatten() +} + +/// map bf custom cells to tablex cells +#let map-cells(cells) = { + cells.map(c => { + assert-bf-cell(c) + + let body = if is-header-cell(c) { + let label_text = c.label.text + let label_num = c.label.num + context { + set text(if c.format.text-size == auto { get-default-header-font-size() } else { c.format.text-size }) + set align(center + bottom) + let size = measure(label_text).width + stack(dir: ttb, spacing: 0pt, + if is-not-empty(label_text) { + box(height: size, inset: (left: 50%, rest: 0pt))[ + #set align(start) + #rotate(c.format.at("angle"), origin: left, label_text) + ] + }, + if (is-not-empty(label_text) and c.format.at("marker") != false){ line(end:(0pt, 5pt)) }, + if c.format.number {box(inset: (top:3pt, rest: 0pt), label_num)}, + ) + } + } else { + { + [ + #if (is-data-cell(c) and (get-default-field-font-size() != auto)) { + [ + #set text(get-default-field-font-size()); + #c.label + ] + } else if (is-note-cell(c) and (get-default-note-font-size() != auto)) { + [ + #set text(get-default-note-font-size()); + #c.label + ] + } else { c.label } + ] + } + } + + return grid.cell( + x: c.position.x, + y: c.position.y, + colspan: c.span.cols, + rowspan: c.span.rows, + inset: c.format.at("inset", default: 0pt), + stroke: c.format.at("stroke", default: 0pt), + fill: c.format.at("fill", default: none), + align: c.format.at("align", default: center + horizon), + body + ) + }) +} + +/// produce the final output +#let generate-table(meta, cells) = { + let table = context { + let rows = if (meta.rows.main == auto) { get-default-row-height() } else { meta.rows.main } + if (type(rows) == array) { rows = rows.map(r => if (r == auto) { get-default-row-height() } else { r } )} + + // somehow grid_header still needs to exists. + let grid_header = if (meta.header != none) { + grid( + columns: (1fr,)*meta.cols.main, + rows: auto, + ..map-cells(cells.filter(c => is-in-header-grid(c))) + ) + } else { none } + + let grid_left = grid( + columns: meta.side.left.cols, + rows: rows, + ..map-cells(cells.filter(c => is-in-left-grid(c))) + ) + + let grid_right = grid( + columns: meta.side.right.cols, + rows: rows, + ..map-cells(cells.filter(c => is-in-right-grid(c))) + ) + + let grid_center = grid( + columns:(1fr,)*meta.cols.main, + stroke: get-default-stoke(), + rows: rows, + ..map-cells(cells.filter(c => is-in-main-grid(c))) + ) + + + return grid( + columns: (if (meta.cols.pre > 0) { auto } else { 0pt }, 1fr, if (meta.cols.post > 0) { auto } else { 0pt }), + inset: 0pt, + ..if (meta.header != none) { + ([/* top left*/], + align(bottom, box( + width: 100%, + fill: if (meta.header.fill != auto) { meta.header.fill } else { get-default-header-background() }, + stroke: if (meta.header.stroke != auto) { meta.header.stroke } else { get-default-header-border() }, + grid_header + )), + [/*top right*/],) + }, + align(top,grid_left), + align(top,grid_center), + align(top,grid_right), + ) + + } + return table +} diff --git a/packages/preview/bytefield/0.0.8/lib/states.typ b/packages/preview/bytefield/0.0.8/lib/states.typ new file mode 100644 index 0000000000..365007b778 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/states.typ @@ -0,0 +1,58 @@ +// states +#let default-row-height = state("bf-row-height", 2.5em); +#let default-header-font-size = state("bf-header-font-size", 9pt); +#let default-field-font-size = state("bf-field-font-size", auto); +#let default-note-font-size = state("bf-note-font-size", auto); +#let default-header-background = state("bf-header-bg", none); +#let default-header-border = state("bf-header-border", none); +#let default-stroke = state("bf-default-stroke", (1pt + black)); + +// function to use with show rule +#let bf-config( + row-height: 2.5em, + field-font-size: auto, + note-font-size: auto, + header-font-size: 9pt, + header-background: none, + header-border: none, + stroke: (1pt + black), + content + ) = { + default-row-height.update(row-height); + default-header-font-size.update(header-font-size) + default-field-font-size.update(field-font-size) + default-note-font-size.update(note-font-size) + default-header-background.update(header-background) + default-header-border.update(header-border) + default-stroke.update(stroke) + content +} + + +#let get-default-row-height() = { + default-row-height.get() +} + +#let get-default-header-font-size() = { + default-header-font-size.get() +} + +#let get-default-field-font-size() = { + default-field-font-size.get() +} + +#let get-default-note-font-size() = { + default-note-font-size.get() +} + +#let get-default-header-background() = { + default-header-background.get() +} + +#let get-default-header-border() = { + default-header-border.get() +} + +#let get-default-stoke() = { + default-stroke.get() +} diff --git a/packages/preview/bytefield/0.0.8/lib/types.typ b/packages/preview/bytefield/0.0.8/lib/types.typ new file mode 100644 index 0000000000..837c7ad301 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/types.typ @@ -0,0 +1,85 @@ +// bf-field +#let bf-field(type, index, data: none) = ( + bf-type: "bf-field", + field-type: type, + field-index: index, + data: data, +) + +// data-field holds information about an field inside the main grid. +#let data-field(index, size, start, end, label, format: none) = { + bf-field("data-field", index, + data: ( + size: size, + range: (start: start, end: end), + label: label, + format: format, + ) + ) +} + +// note-field holds information about an field outside (left or right) the main grid. +#let note-field(index, side, level:0, label, format: none, row: auto, rowspan: 1) = { + bf-field("note-field", index, + data: ( + side: side, + row: row, + rowspan: rowspan, + level: level, + label: label, + format: format, // TODO + ) + ) +} + +// header-field hold information about a complete header row. Usually this is the top level header. +#let header-field(start: auto, end: auto, autofill: auto, numbers: (), labels: (:), ..args) = { + // header-field must have index 0. + bf-field("header-field", none, + data: ( + // This is at the moment always 0 - (bpr), but in the future there might be header fields between data rows. + range: (start: start, end: end), + // Defines which numbers should be shown. Possible none or array if numbers. + numbers: numbers, + // Defines which labels should be shown. Dict of number and content. + labels: labels, + // Defines which numbers should be calculated automatically + autofill: autofill, + // Defines the format of the bitheader. + format: ( + // Defines the angle of the labels + angle: args.named().at("angle", default: -60deg), + // Defines the text-size for both numbers and labels. + text-size: args.named().at("text-size",default: auto), + // Defines if a marker should be shown + marker: args.named().at("marker", default: true), + // Defines the background color. + fill: args.named().at("fill", default: none), + // Defines the border stroke. + stroke: args.named().at("stroke", default: none), + ) + ) + ) +} + +// bf-cell holds all information which are necessary for cell positioning inside the table. +#let bf-cell(type, grid: center, x: auto, y: auto, colspan:1, rowspan:1, label: none, cell-index: (auto, auto) ,format: auto) = ( + bf-type: "bf-cell", + cell-type: type, + // cell index is a tuple (field-index, slice-index) + cell-index: cell-index, + // position specifies the grid and and the position inside the grid. + position: ( + grid: grid, + x: x, + y: y, + ), + span: ( + rows: rowspan, + cols: colspan, + ), + // The text which will be shown. + label: label, + // specified format for the label like fill, stroke, align, inset, ... + format: format, +) \ No newline at end of file diff --git a/packages/preview/bytefield/0.0.8/lib/utils.typ b/packages/preview/bytefield/0.0.8/lib/utils.typ new file mode 100644 index 0000000000..65ff422ec9 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/lib/utils.typ @@ -0,0 +1,140 @@ +#import "asserts.typ": * + +// insert a value into an dict and return the dict +#let dict-insert-and-return(dict, key, value) = { + assert.eq(type(dict), dictionary); + assert.eq(type(key), str); + dict.insert(key,value); + return dict +} + +#let get-field-type(field) = { + assert-bf-field(field); + return field.at("field-type", default: none); +} + +/// Check if a given field is a bf-field +#let is-bf-field(field) = { + assert.eq(type(field), dictionary) + field.at("bf-type", default: none) == "bf-field" +} + +/// Check if a given field is a data-field +#let is-data-field(field) = { + assert-bf-field(field); + field.at("field-type", default: none) == "data-field" +} + +/// Check if a given field is a note-field +#let is-note-field(field) = { + assert-bf-field(field); + field.at("field-type", default: none) == "note-field" +} + +/// Check if a given field is a header-field +#let is-header-field(field) = { + assert-bf-field(field); + field.at("field-type", default: none) == "header-field" +} + +/// Return the index of the next data-field +#let get-index-of-next-data-field(idx, fields) = { + let res = fields.sorted(key: f => f.field-index).find(f => f.field-index > idx and is-data-field(f)) + if res != none { res.field-index } else { none } +} + +/// Check if a field spans over multiple rows +#let is-multirow(field, bpr) = { + // if range.start is at the beginn of a new row + if (calc.rem(field.data.range.start, bpr) != 0) { return false } + // field size is multiple of bpr + if (calc.rem(field.data.size, bpr) != 0) { return false } + + return true +} + +/// calculates the max annotation level for both sides +#let get-max-annotation-levels(annotations) = { + let left_max_level = -1 + let right_max_level = -1 + for field in annotations { + assert-bf-field(field) + let (side, level, ..) = field.data; + if (side == left) { + left_max_level = calc.max(left_max_level,level) + } else { + right_max_level = calc.max(right_max_level,level) + } + } + return ( + pre-levels: left_max_level +1, + post-levels: right_max_level +1, + ) +} + +/// Check if given dict is a bf-cell +#let is-bf-cell(cell) = { + cell.at("bf-type", default: none) == "bf-cell" +} + +/// Check if bf-cell is a data cell. +#let is-data-cell(cell) = { + cell.at("cell-type", default: none) == "data-cell" +} + +/// Check if bf-cell is a note cell. +#let is-note-cell(cell) = { + cell.at("cell-type", default: none) == "note-cell" +} + +/// Check if bf-cell is a header cell. +#let is-header-cell(cell) = { + cell.at("cell-type", default: none) == "header-cell" +} + +/// Check if a string is empty +#let is-empty(string) = { + assert.eq(type(string), str, message: "expected string, found " + type(string)) + string == none or string == "" +} + +/// Check if a string or content is not empty +#let is-not-empty(string) = { + // assert.eq(type(string), str, message: strfmt("expected string, found {}",type(string))) + if (type(string) == str) { + return string != none and string != "" + } else if (type(string) == content) { + return string != [] + } +} + +/// Return the current index of a field or cell +#let get-index(f) = { + if is-bf-field(f) { + return f.field-index + } else if is-bf-cell(f) { + return f.cell-index + } else { + none + } +} + +/// Check if in a certain grid +#let is-in-main-grid(cell) = { + center == cell.position.grid +} + +/// Check if in a certain grid +#let is-in-left-grid(cell) = { + left == cell.position.grid +} + +/// Check if in a certain grid +#let is-in-right-grid(cell) = { + right == cell.position.grid +} + +/// Check if in a certain grid +#let is-in-header-grid(cell) = { + top == cell.position.grid +} diff --git a/packages/preview/bytefield/0.0.8/typst.toml b/packages/preview/bytefield/0.0.8/typst.toml new file mode 100644 index 0000000000..1413a0fa68 --- /dev/null +++ b/packages/preview/bytefield/0.0.8/typst.toml @@ -0,0 +1,19 @@ +[package] +name = "bytefield" +version = "0.0.8" +entrypoint = "bytefield.typ" +authors = ["Jomaway "] +license = "MIT" +description = "A package to create network protocol headers, memory map, register definitions and more." +repository = "https://github.com/jomaway/typst-bytefield" +keywords = ["bytefields", "protocol", "header", "memory", "map", "register"] +compiler = "0.14.0" +exclude = [ + "examples/**", + "docs/**", + "**.pdf", + "**.png", + "tests/*", + "justfile", + "CHANGELOG.md", +]