This commit is contained in:
2025-05-19 23:08:28 +02:00
commit 4d6c7201ee
15 changed files with 842 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
build
pub.sh
*.html
*.pdf

20
build.sh Executable file
View File

@@ -0,0 +1,20 @@
set -e
rm -rf build
mkdir build
compile () {
typst compile --root . --features html -j 4 $@
}
page () {
compile --format pdf --input web=false "pages/$1" "build/$1.min.pdf"
compile --format html --input web=true "pages/$1" "build/$1.desktop.html"
compile --format html --input web=false "pages/$1" "build/$1.min.html"
}
page "article-make-regex-engine-1.typ"
page "project-etc-nand.typ"
page "index.typ"
cp build/index.typ.desktop.html build/index.html

243
common.typ Normal file
View File

@@ -0,0 +1,243 @@
#let to-bool(str) = {
if str == "true" {
return true
}
if str == "false" {
return false
}
assert(false)
}
#let is-web = to-bool(sys.inputs.at("web", default: "true"))
#let is-html() = { return target() == "html" }
#let res-path() = {
if is-html() {
"res/"
} else {
"/res/"
}
}
#let title(content) = {
context if is-html() {
html.elem("h1", content)
} else {
text(23pt, content)
}
}
#let br() = {
context if is-html() {
html.elem("br")
}
"\n"
}
#let space(num: 1) = {
context if is-html() {
[~] * (num - 1) + [ ]
} else {
" " * num
}
}
#let html-p(txt) = {
context if is-html() {
html.elem("p", txt)
} else {
text(txt)
}
}
#let sized-p(size, txt) = {
context if is-html() {
html.elem("p", attrs: (style: "font-size: " + str(size.abs.pt()) + "pt"), txt)
} else {
text(size, txt)
}
}
#let html-frame(content) = {
context if is-html() {
html.frame(content)
} else {
content
}
}
#let html-opt-elem(kind, attrs, content) = {
context if is-html() {
html.elem(kind, attrs: attrs, content)
} else {
content
}
}
#let html-span(attrs, content) = {
html-opt-elem("span", attrs, content)
}
#let html-bold(content) = {
context if is-html() {
html.elem("b", content)
} else {
content
}
}
#let html-style(class:"", style, content) = {
html-span((class:class, style: style), content)
}
#let slink(target, link-text) = text(fill: blue)[
#underline[#link(target, link-text)]
]
#let flink(target, link-text) = text(fill: blue)[
#underline[#link(target, link-text)]
#footnote[#text(fill: blue)[#link(target)]]
]
#let section(b) = block(breakable: false, [
\
#b
])
#let take-while(array, fn) = {
let out = ();
for x in array {
if fn(x) {
out += (x,);
} else {
break;
}
}
return out;
}
#let gen-tree-from-headings(
hide-first: false,
marker: context "├─" + space(),
marker-end: context "└─" + space(),
side: context "|" + space(num:2),
side-empty: context space(num:3),
elemfn: x => x,
headings,
) = {
for (idx, item) in headings.enumerate() {
let content = [];
for ind in range(item.level - 1) {
let will-future = take-while(headings.slice(idx),
x => x.level > ind)
.any(x => x.level == ind + 1);
content += if will-future {
side
} else {
side-empty
}
}
content += if take-while(headings.slice(idx + 1),
x => x.level >= item.level)
.any(x => x.level == item.level) {
marker
} else {
marker-end
}
elemfn(content, item)
}
}
#let len2css(len, default: "auto", map-pt: x => str(x)+"pt") = {
if len == auto or type(len) == dictionary {
return default
}
if type(len) == relative and float(len.ratio) * 100 != 0 {
assert(len.length.pt() == 0)
return str(float(len.ratio) * 100) + "%";
}
if type(len) == relative {
len = len.length
}
return map-pt(len.abs.pt())
}
#let option-map(option, default, fn) = {
if option == none { default }
else { fn(option) }
}
#let stroke2css(stroke) = {
if type(stroke) == dictionary {
return "none"
}
let th = len2css(stroke.thickness, default: "1pt")
return th + " solid black" // TODO: paint
}
#let css-style(it) = {
return "
display: inline-block;
border: "+stroke2css(it.stroke)+";
border-radius: "+len2css(it.radius, default: "0px")+";
" + option-map(len2css(it.width, default:none), "", x => "width:"+x+";") +"
" + option-map(len2css(it.height, default:none), "", x => "height:"+x+";") +"
padding: " + len2css(it.inset)
}
#let html-script(code) = {
[#context if is-html(){
html.elem("script", code)
}]
}
#let html-script-dom-onload(code) = {
html-script("document.addEventListener('DOMContentLoaded', function() { "+code+" })")
}
#let column-fixed(..contents) = {
html-style(class:"column-fixed", "display: inline-flex; position: fixed; justify-content: center; width: 25%")[
#table(..contents)
#context if is-html() {
html.elem("style", "
.column-fixed > table > tbody > tr > td > * { width: 100%; }
")
}
]
}
#let table-of-contents() = {
html-style(class:"table-of-contents", "", [
#box(
stroke: black,
radius: 2pt,
inset: 3%,
)[
Table of contents\
#context {
let idx = state("stupid-gen-page-state", 0);
gen-tree-from-headings(query(heading),
elemfn: (content, x) => {
[#content #context html-span((class:"headingr",id:"headingr-"+str(idx.get())), link(x.location(), x.body)) #context idx.update(x=> x + 1) #br()]
})
}
]
#html-script-dom-onload("
let tags = ['h2','h3','h4'].flatMap(x => Array.from(document.getElementsByTagName(x)));
document.getElementById('headingr-0').classList.add('current')
document.addEventListener('scroll', (event) => {
let curr = tags.map(x => [x, (x.getBoundingClientRect().top + x.getBoundingClientRect().bottom) / 2]).filter(x => x[1] >= 0).sort((a,b) => a[1] > b[1])[0][0];
let idx = tags.sort((a,b) => a.getBoundingClientRect().top > b.getBoundingClientRect().top).map((x, idx) => [idx, x]).filter(x => x[1] == curr)[0][0];
Array.from(document.getElementsByClassName('headingr')).map(x => x.classList.remove('current'))
document.getElementById('headingr-'+idx).classList.add('current')
});
")
] + [
#context if is-html() {
html.elem("style", "
.table-of-contents > p > span { width: 100%; }
")
}
])
}

24
components/pcb-view.typ Normal file
View File

@@ -0,0 +1,24 @@
#import "../common.typ": *
#let pcb(front,back, size-percent:100) = {
context if is-html() {
if is-web {
html.elem("img", attrs:(draggable:"false", tite:"Click Me!", src:front, data-other:back, onclick:"swapFrontBack(this);", style:"width:"+str(size-percent)+"%; cursor:pointer;"))
} else {
html.elem("img", attrs:(src:front, style:"width:100%;"))
}
} else {
[#image(front) #image(back)]
}
context if is-html() and is-web {
html-script("
function swapFrontBack(img) {
let oldsrc = img.src;
img.src = img.dataset.other;
img.dataset.other = oldsrc;
}
")
}
}

115
core-page-style.typ Normal file
View File

@@ -0,0 +1,115 @@
#import "common.typ": *
#let small-font-size = 14pt
#let default-font-size = 17pt
#let core-page-style(content) = {[
#show: x => context {
set page(width: auto, height: auto) if is-web and not is-html()
set page(paper: "a4") if not is-web and not is-html()
x
}
#set text(font: "DejaVu Sans Mono", size: default-font-size)
#show raw: it => box(
stroke: black,
radius: 2pt,
inset: if is-html() { 1.4pt } else { 4pt },
outset: 0pt,
baseline: 3.1pt,
text(it)
)
#show box: it => {
context if is-html() {
html.elem("span", attrs: (style: css-style(it)))[#it.body]
} else {
it
}
}
#show underline: it => {
context if is-html() {
html.elem("u", it.body)
} else {
it
}
}
#show heading: it => underline[#it #v(3pt)]
#set underline(stroke: 1pt, offset: 2pt)
#show footnote: it => if is-web { [] } else { it }
#show footnote.entry: it => if is-web { [] } else { it }
#context if is-html() {
html.elem("style", "
@font-face {
font-family: 'DejaVu Sans Mono';
src: url('res/DejaVuSansMono-Bold.woff2') format('woff2'),
url('res/DejaVuSansMono-Bold.woff') format('woff'),
local('DejaVu Sans Mono'),
local('Courier New'),
local(Courier),
local(monospace);
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'DejaVu Sans Mono';
src: url('res/DejaVuSansMono.woff2') format('woff2'),
url('res/DejaVuSansMono.woff') format('woff'),
local('DejaVu Sans Mono'),
local('Courier New'),
local(Courier),
local(monospace);
font-weight: normal;
font-style: normal;
font-display: swap;
}
body {
font-family: DejaVu Sans Mono;
font-size: "+len2css(default-font-size)+";
}
td {
width: 100%;
display: inline;
vertical-align: top;
}
h1,h2,h3,h4 {
margin-top: 1%;
margin-bottom: 0.75%;
" + if is-web { "margin-left: -0.75%;" } else { "" }
+
"
}
p {
margin-top: 0.75%;
margin-bottom: 0.75%;
}
ul {
margin-top: 0%;
}
.current {
font-weight: bold;
}
")
}
#content
]
}

View File

@@ -0,0 +1,224 @@
#import "../common.typ": *
#import "../simple-page-layout.typ": *
#import "../core-page-style.typ": *
#simple-page(
gen-table-of-contents: true
)[
#section[
#title[Making a simple RegEx engine]
#title[Part 1: Introduction to RegEx]
#sized-p(small-font-size)[
Written by alex_s168
]
]
#if is-web {section[
Note that the #min-pdf-link[PDF Version] of this page might look a bit better styling wise.
]}
#section[
= Introduction
If you are any kind of programmer,
you've probably heard of #flink("https://en.wikipedia.org/wiki/Regular_expression", "RegEx")
RegEx (Regular expression) is kind of like a small programming language
used to define string search and replace patterns.
\
RegEx might seem overwhelming at first, but you can learn the most important features of RegEx very quickly.
\
It is important to mention that there is not a single standard for RegEx syntax,
but instead each "implementation" has it's own quirks, and additional features.
Most common features however behave identically on most RegEx "engines"/implementations.
]
#section[
= Syntax
The behavior of RegEx expressions / patterns depends on the match options passed to the RegEx engine.
Common match options: <match-options>
- Anchored at start and end of line
- Case insensitive
- multi-line or instead whole string
]
#section[
== "Atoms"
In this article, we will refer to single expression parts as "atoms".
]
#section[
=== Characters
Just use the character that you want to match. For example ```re a``` to match an `a`.
This however does not work for all characters, because many are part of special RegEx syntax.
]
#section[
=== Escaped Characters <escaped-chars>
Thee previously mentioned special characters like `[` can be matched by putting a backslash in front of them: ```re \[```
#context html-frame[
#table(
columns: (auto,auto),
table.header(
[Pattern],
[Description]
),
[```re \\```], [match a literal backslash],
[```re \n```], [match a new-line],
)
]
]
#section[
=== Character Groups <char-groups>
RegEx engines already define some groups of characters that can make writing RegEx expressions quicker.
#context html-frame[
#table(
columns: (auto,auto),
table.header(
[Pattern],
[Description],
),
[```re .```], [any character except for line breaks],
[```re \s```], [any whitespace or line break],
[```re \S```], [any character except whitespaces or line breaks],
[```re \d```], [any digit from 0 to 9],
[```re \D```], [any character except digits from 0 to 9],
[```re \w```], [a letter, digit, or underscore],
[```re \W```], [any character except for letters, digits, and underscores],
)
]
]
#section[
=== Anchors
```re ^``` is used to assert the beginning of a line in multi-line mode,
or the beginning of the string in whole-string mode.
```re $``` is used to assert the end of a line in multi-line mode,
or the end of the string in whole-string mode.
The behaviours of these depend on the #slink(<match-options>)[match options]
]
#section[
== Greedy VS Lazy <greedy>
Some combinators will either match "lazy", or "greedy".
Lazy is when the engine only matches as many characters required to get to the next step.
This should almost always be used.
Greedy matching is when the engine tries to match as many characters as possible.
The problem with this is that it might cause "backtracking",
which happens when the engine goes back in the pattern multiple times to ensure that as many characters
as possible where matched. This can cause big performance issues.
]
#section[
== Combinators
Multiple atoms can be combined together to form more complex patterns.
]
#section[
=== Chain
When two expressions are next to each other, they will be chained together,
which means that both will be evaluated in-order.
Example: ```re x\d``` matches a `x` and then a digit, like for example `x9`
]
#section[
=== Or
Two expressions separated by a `|` cause the RegEx engine to first try to match the left side,
and only if it fails, it tries the right side instead.
Note that "or" has a long left and right scope,
which means that ```re ab|cd``` will match either ```re ab``` or ```re cd```
]
#section[
=== Or-Not
Tries to match the expression on the left to it, but won't error if it doesn't succeed.
Note that "or-not" has a short left scope,
which means that ```re ab?``` will always match ```re a```, and then try to match ```re b```
]
#section[
=== Repeated
A expression followed by either a ```re *``` for #slink(<greedy>)[greedy] repeat,
or a ```re *?``` for #slink(<greedy>)[lazy] repeat.
This matches as many times as possible, but can also match the pattern zero times.
Note that this has a short left scope.
]
#section[
=== Repeated At Least Once
A expression followed by either a ```re +``` for #slink(<greedy>)[greedy] repeat,
or a ```re +?``` for #slink(<greedy>)[lazy] repeat.
This matches as many times as possible, and at least one time.
Note that this has a short left scope.
]
#section[
=== (Non-Capture) Group <non-capture-group>
Groups multiple expressions together for scoping.
Example: ```re (?:abc)``` will just match `abc`
]
#section[
=== Capture Group
Similar to #slink(<non-capture-group>)[Non-Capture Groups] except that they capture the matched text.
This allows the matched text of the inner expression to be extracted later.
Capture group IDs are enumerated from left to right, starting with 1.
Example: ```re (abc)de``` will match `abcde`,
and store `abc` in group 1.
]
#section[
=== Character Set
By surrounding multiple characters in square brackets,
the engine will match any of them.
Special characters or expressions won't be parsed inside them,
which means that this can also be used to escape characters.
For example: ```re [abc]``` will match either `a`, `b` or `c`.
and ```re [ab(?:c)]``` will match either `a`, `b`, `(`, `?`, `:`, `c`, or `)`.
#slink(<char-groups>)[Character groups] and #slink(<escaped-chars>)[escaped characters]
still work inside character sets.
Character sets can also contain ranges.
For example: ```re [0-9a-z]``` will match either any digit, or any lowercase letter.
]
#section[
= Conclusion
RegEx is perfect for when you just want to match some patterns,
but the syntax can make patterns very hard to read or modify.
In the next article, we will start to dive into implementing RegEx.
Stay tuned!
]
]

81
pages/index.typ Normal file
View File

@@ -0,0 +1,81 @@
#import "../common.typ": *
#import "../simple-page-layout.typ": *
#let gen-page(content) = {
core-page-style[
#if is-web {
table(
stroke: none,
columns: (25%, 50%, 25%),
[],
[
#html-style("position: absolute; left: 28%; width: 100%")[
#box(width: 50%, content)
]
],
)
} else {
content
}
]
}
#let tree-list(..elements) = {
gen-tree-from-headings(elemfn: (content, x) => [
#html-opt-elem("p", (style:"line-height:1.1"))[
#html-style("display:flex; text-indent:0pt;")[
#html-style("margin-right: 11pt;", content)
#html-style("flex:1;", x.body)
]
]
], elements.pos())
}
#gen-page[
#br()
#title[alex_s168's]
#br()
Articles
#br()
#tree-list(
(level:1, body: [ Making a simple RegEx engine ]),
(level:2, body: html-href("article-make-regex-engine-1.typ.desktop.html")[ Part 1: Introduction to RegEx ]),
)
#br()
Socials
#br()
#tree-list(
(level:1, body: html-href("https://github.com/alex-s168")[ GitHub ]),
(level:1, body: [Discord: alex_s168]),
(level:1, body: html-href("mailto:alexandernutz68@gmail.com")[ E-Mail ]),
(level:1, body: html-href("https://codeberg.org/alex-s168")[ Codeberg ]),
)
#br()
Working on
#br()
#tree-list(
(level:1, body: [ Programming languages and compilers ]),
(level:2, body: [ #link("https://github.com/vxcc-backend/vxcc")[ vxcc-old ]: (discontinued) Simple optimizing compiler backend ]),
(level:2, body: [ #link("https://github.com/alex-s168/uiuac")[ uiuac ]: (discontinued) Optimizing compiler for the #link("https://uiua.org")[Uiua programming language] ]),
(level:2, body: [ #link("https://github.com/Lambda-Mountain-Compiler-Backend/lambda-mountain")[ LSTS's standard library ] ]),
(level:2, body: [ #link("https://github.com/h6-lang/h6")[ h6 ]: Minimal stack-based programming language ]),
(level:2, body: [ #link("https://github.com/alex-s168/lil-rs")[ lil-rs ]: Rust implementation of #link("http://beyondloom.com/decker/lil.html")[lil] ]),
(level:1, body: [ Misc. ]),
(level:2, body: [ #link("https://github.com/alex-s168/tpre")[ tpre ]: Fast and minimal RegEx engine ]),
(level:1, body: [ PCBs ]),
(level:2, body: [ #link("project-etc-nand.typ.desktop.html")[ etc-nand ]: #link("https://github.com/ETC-A/etca-spec/")[ ETC.A ] CPU from NAND gates ]),
(level:1, body: [ FPGA designs ]),
(level:2, body: [ RMII MAC in #link("https://www.chisel-lang.org/")[ Chisel ] ]),
)
#br()#br()
This website is written almost entierly in typst.
]

View File

@@ -0,0 +1,84 @@
#import "../common.typ": *
#import "../simple-page-layout.typ": *
#import "../components/pcb-view.typ": *
#let pcb-size-percent = 80
#let qpcb(file) = {
let p = res-path()+"etc-nand/"+file
pcb(p+"_front.png", p+"_back.png", size-percent: pcb-size-percent)
}
#simple-page(
gen-table-of-contents: true
)[
#section[
#title[ etc-nand ]
]
#if is-web {section[
Note that the #min-pdf-link[PDF Version] of this page might look a bit better styling wise.
You can click the PCB images to switch to the other side.
]}
#section[
= Overview
etc-nand is a real-world #link("https://github.com/ETC-A/etca-spec/")[ ETC.A ] CPU built from almost only quad NAND gate ICs (74hc00)
It will probably be finished in a few months.
]
#section[
== Estimates
Estimated gate count:
- 2800 NAND gates
- 320 tristate buffers
#br()
Estimated component counts:
- 700x 74hc00 quad NAND gates
- 40x 74HC54 octal tristate buffers
- a few simple resistors
]
#section[
== Planned Specifications
ETC.A base instruction set + byte operations + S&F + Von Neumann
The CPU will communicate with peripherals over a 16 bit data + 15 bit address memory bus
]
#section[
= Purchase
You will be able to purchase one in the future.
Stay tuned!
]
#section[
= Images
Images of PCBs that are either already manifactured or currently beeing manifactured by JLCPCB.
]
#section[
== 16 bit register
#context qpcb("reg16")
]
#section[
== 8 bit ALU slice
A #link(<add8>)[8 bit adder module] will be placed in the middle
#context qpcb("alu8")
]
#section[
== 8 bit adder <add8>
#context qpcb("add8")
]
]

BIN
res/etc-nand/add8_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
res/etc-nand/add8_front.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
res/etc-nand/alu8_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
res/etc-nand/alu8_front.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
res/etc-nand/reg16_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

47
simple-page-layout.typ Normal file
View File

@@ -0,0 +1,47 @@
#import "common.typ": *
#import "core-page-style.typ": *
#let html-href(target, content) = {
[#context if is-html() {
html.elem("a", attrs:(href:target), content)
} else { content }]
}
#let min-pdf-link(content) = {
[#context if is-html() {
html.elem("a", attrs:(href:"#",onclick:"gotoMinPdf();"), content)
} else { content }]
}
#let simple-page(gen-table-of-contents: true, gen-index-ref: true, content) = {
core-page-style[
#if is-web {
table(
stroke: none,
columns: (25%, 50%, 25%),
column-fixed(
[#if gen-table-of-contents { [#table-of-contents()] }],
min-pdf-link("Minimal PDF Version"),
[#if gen-index-ref {[
#context br()
#context html-href("index.html")[#html-bold[Website Home]]
]}]
),
[
#let off = 3;
#html-style("position: absolute; left: "+str(25+off)+"%; width: "+str(75-off)+"%")[
#box(width: 50%, content)
]
],
)
} else {
content
}
#html-script("
function gotoMinPdf() {
window.location.href = window.location.href.replace(/\.\w+.html/g, '.min.pdf');
}
")
]
}