initial
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.quarto
|
47
About/Experience.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
status: Incomplete
|
||||||
|
---
|
||||||
|
# Highlights of my experiences in the programming world
|
||||||
|
|
||||||
|
(as far as NDA and other things allow it)
|
||||||
|
|
||||||
|
## Haskell-Enthusiast
|
||||||
|
|
||||||
|
- Learning/Writing Haskell since ~2014
|
||||||
|
- Created and held advanced Haskell-Lecture at my University
|
||||||
|
|
||||||
|
### github
|
||||||
|
|
||||||
|
- [My Profile](https://github.com/Drezil/)
|
||||||
|
- [Haskell-Lecture](https://github.com/FFPiHaskell/)
|
||||||
|
- [Co-Founder of DataHaskell](https://github.com/DataHaskell)
|
||||||
|
|
||||||
|
## gitea
|
||||||
|
|
||||||
|
I also have a [gitea-instance](https://gitea.dresselhaus.cloud/explore/repos)
|
||||||
|
where one can finde more current things and backups of old.
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- **Author** of Eve-Online-Interface in [yesod-auth-oauth2](https://github.com/thoughtbot/yesod-auth-oauth2/pull/33)
|
||||||
|
- **Author** of "New Eden Accounting Tool" ([neat](https://github.com/Drezil/neat)),
|
||||||
|
which is basically a ledger for Trading in the game Eve-Online
|
||||||
|
- Driver behind getting [https://github.com/jgm/pandoc/issues/168]() implemented
|
||||||
|
and merged, because we needed it for our slide-filters (see [[Work]]# ->
|
||||||
|
Development of Filters)
|
||||||
|
- **Author** of [img2ascii](https://github.com/Drezil/img2ascii) - Small cli-tool
|
||||||
|
for converting images into terminal-codes & ascii using JuicyPixels, because i
|
||||||
|
always forget what is on the images over an ssh-connection -.-
|
||||||
|
- **Implemented Array-Fusion and Recycling** for [subhask](https://github.com/mikeizbicki/subhask/pull/57)
|
||||||
|
as layed out in [Recycle your Arrays](https://doi.org/10.1007/978-3-540-92995-6_15)
|
||||||
|
by Roman Leshchinskiy
|
||||||
|
- [**Raytracer** in Haskell for my Computergraphics-Course](https://github.com/Drezil/htrace)
|
||||||
|
- **implementation of [Densely Connected Bi-Clusters](https://github.com/Drezil/hgraph)-Algorithm** in Haskell
|
||||||
|
([Paper](https://www.researchgate.net/profile/Recep_Colak/publication/267918524_DENSELY-CONNECTED_BI-CLUSTERING/links/560f1aff08ae483375178a03.pdf))
|
||||||
|
- [Chemodiversity-Project](https://gitea.dresselhaus.cloud/Drezil/chemodiversity)
|
||||||
|
at University during my masters. Complete with slideshow explaining
|
||||||
|
everything.
|
||||||
|
- several other dead projects :D
|
||||||
|
|
57
About/Extracurricular.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
status: Done
|
||||||
|
---
|
||||||
|
# Studium generale / University-Life
|
||||||
|
|
||||||
|
(What I did at university besides studying :sunglasses: )
|
||||||
|
|
||||||
|
## Committees / Student Body
|
||||||
|
|
||||||
|
- Student Member of Studienbeirat Informatik (Study-Profile Commission)
|
||||||
|
- Student Member of Tutorenauswahlkommission (Tutor-Selection Committee)
|
||||||
|
- Leader Tutorenevaluation (Evaluation of Tutors)
|
||||||
|
- Student Member of NWI-Master-Auswahlausschuss (Master-Application Committee for
|
||||||
|
my course of study)
|
||||||
|
- Student Member of NWI-Master-Prüfungsausschuss (Committee for Exam-disputes of
|
||||||
|
my Master course)
|
||||||
|
- Member of the Admin-Team for the student-body pcs
|
||||||
|
|
||||||
|
## ekvv-Links (entries in the electronic course-catalog)
|
||||||
|
|
||||||
|
### Summer 15
|
||||||
|
|
||||||
|
- [Fortgeschrittene funktionale Programmierung in Haskell](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=54004629)
|
||||||
|
(Haskell-Lecture)
|
||||||
|
- [Lecture on YouTube](https://www.youtube.com/playlist?list=PLMqFm6rr-xOWhXGroUXzWx00FeaBNfbsa)
|
||||||
|
- [[FFPiH|more details on the lecture]]#
|
||||||
|
|
||||||
|
### Summer 16
|
||||||
|
|
||||||
|
- [Fortgeschrittene funktionale Programmierung in Haskell](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=71172682)
|
||||||
|
(Haskell-Lecture)
|
||||||
|
- [Lecture on YouTube](https://www.youtube.com/playlist?list=PLMqFm6rr-xOUEf2YjSxRn8BIhrdRIhZw6)
|
||||||
|
(differs from link above)
|
||||||
|
- This was the **"silver chalk"-lecture**
|
||||||
|
- [[FFPiH|more details on the lecture]]#
|
||||||
|
|
||||||
|
### Winter 16/17
|
||||||
|
|
||||||
|
- [Richtig Starten](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=84763664)
|
||||||
|
(Start Right!)
|
||||||
|
- [Tutor Introduction to Machine Learning](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=79599350)
|
||||||
|
(Tutor in this Lecture)
|
||||||
|
- Was awarded **Tutoring-Award** of the faculty
|
||||||
|
- Remade and updated slides for [Computergraphics-Lecture](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=79016005)
|
||||||
|
- Lecture was **awarded "silver chalk"** among others things because of the
|
||||||
|
updated slides.
|
||||||
|
|
||||||
|
### Summer 17
|
||||||
|
|
||||||
|
- [Fortgeschrittene funktionale Programmierung in Haskell](https://ekvv.uni-bielefeld.de/kvv_publ/publ/vd?id=94694136)
|
||||||
|
(Haskell-Lecture)
|
||||||
|
- Same as Summer 16
|
||||||
|
- Totally **reworked Exercises** accompanying the lecture
|
||||||
|
- [[FFPiH|more details on the lecture]]#
|
||||||
|
|
BIN
About/Nicole.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
About/Nicole_small.png
Normal file
After Width: | Height: | Size: 311 KiB |
86
About/Work.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
status: Incomplete
|
||||||
|
---
|
||||||
|
# Work-Experience
|
||||||
|
|
||||||
|
- **Mar. 2023 to Sep. 2023:**
|
||||||
|
- Developer for 2Lambda.co. Role migrated from just coding stuff to
|
||||||
|
architecting and rewriting the whole software from the ground up using a
|
||||||
|
small modular approach instead of the shaky one-off systems in place.
|
||||||
|
Was later a "nanny for everything".
|
||||||
|
- Did a lot of work to have self-documenting code (i.e. generate documentation
|
||||||
|
from the actual values used in the program, not some comments that always
|
||||||
|
get out of date)
|
||||||
|
- Setting up a knowledge-base (Zettelkasten-approach) to track experiments and
|
||||||
|
hyperlink them to the documentation generated above (and due to Zettelkasten
|
||||||
|
you then get "this thing was used in Experiments a, b and c" automatically
|
||||||
|
- Technologies used:
|
||||||
|
- Clojure
|
||||||
|
- Complete application was written in Clojure
|
||||||
|
- Never touched that language before March - got up to speed in just 2
|
||||||
|
days, poked the expert on the team detailed questions about the
|
||||||
|
runtime-system after 1 month (like inlining-behavior, allocation-things,
|
||||||
|
etc.)
|
||||||
|
- Emanote
|
||||||
|
- autogenerated & linked documentation of internal modules
|
||||||
|
- integrated with manual written tutorials/notes
|
||||||
|
- crosslinking documentation of experiments with documentation of modules
|
||||||
|
- Web of knowledge
|
||||||
|
- bidirectional discovery of things tried/done in the past to optimize
|
||||||
|
finding of new strategies (meta-optimizing the decisions on what to
|
||||||
|
optimize/try)
|
||||||
|
- Infrastructure
|
||||||
|
- Organized and co-administrated the 4 Root-Servers we had
|
||||||
|
- Set up Kubernetes, Nexus, Docker, Nginx, letsencrypt-certs, dns-entries,
|
||||||
|
etc..
|
||||||
|
|
||||||
|
- **Oct. 2018 to Aug. 2021**:
|
||||||
|
- ML-Specialist at [Jobware](https://jobware.de) (Paderborn; german Job-Advertising-Platform)
|
||||||
|
- Extraction/Classification of sentences from JobAds (Requirements, Benefits,
|
||||||
|
Tasks, ...)
|
||||||
|
- Extraction of Information from JobAds (Location of company, Location of
|
||||||
|
workplay, contact-details, application-procedure, etc.) including geocoding
|
||||||
|
of those information (backed by OpenStreetMap)
|
||||||
|
- Embedding of JobAds into a meaningful space (i.e. "get me similar ads. btw.
|
||||||
|
i dislike ad a, b, c").
|
||||||
|
- Analyse & predict search-queries of users on the webpage and offer likely
|
||||||
|
but distinct queries (i.e. similar when typo or complete different words
|
||||||
|
(synonyms, hyponyms, etc.))
|
||||||
|
- Technologies used:
|
||||||
|
- Haskell (currently GHC 8.6, soon GHC 8.8)
|
||||||
|
- stack + stackage-lts
|
||||||
|
- fixplate (recursion-schemes-implementation)
|
||||||
|
- many usual technologies like lens, http-simple, mtl, ..
|
||||||
|
- golden-testing via tasty
|
||||||
|
- several inhouse-developments:
|
||||||
|
- templating based on text-replacement via generics (fieldname in
|
||||||
|
Template-Type == variable replaced in template)
|
||||||
|
- activeMQ/Kibana-bridge for logging via hs-stomp
|
||||||
|
- generic internal logging-framework
|
||||||
|
- Python
|
||||||
|
- tensorflow
|
||||||
|
- pytorch
|
||||||
|
- sklearn
|
||||||
|
- nltk
|
||||||
|
|
||||||
|
- **2013-2018**:
|
||||||
|
- several jobs at my University including
|
||||||
|
- Worked 6 Months in the Workgroup "Theoretical Computer Science" on migrating
|
||||||
|
algorithms to **CUDA**
|
||||||
|
- Tutor "Introduction to Machine Learning"
|
||||||
|
- Was awarded **Tutoring-Award** of the Faculty of Technology for excellent
|
||||||
|
tutoring
|
||||||
|
- Lecture "[[FFPiH|Intermediate Functional Programming in Haskell]]"
|
||||||
|
- Originally developed as student-project in cooperation with Jonas Betzendahl
|
||||||
|
- First held in Summer 2015
|
||||||
|
- Due to high demand held again in Summer 2016 and 2017
|
||||||
|
- Was awarded **Lecturer-Award** "silver Chalk" in 2016
|
||||||
|
- First time that this award was given to students
|
||||||
|
- Many lecturers at our faculty never get any teaching-award until retirement
|
||||||
|
- Development of Pandoc-Filters for effective **generation of lecture-slides**
|
||||||
|
for Mario Botsch (Leader "Workgroup Computer Graphics") using Pandoc & reveal.js
|
||||||
|
- Framework: [https://github.com/mbotsch/revealSlides](https://github.com/mbotsch/revealSlides)
|
||||||
|
- Example: [https://github.com/mbotsch/eLearning](https://github.com/mbotsch/eLearning)
|
||||||
|
- Pandoc-Filters: [https://github.com/mbotsch/pandoc-slide-filter](https://github.com/mbotsch/pandoc-slide-filter)
|
BIN
About/avatar_neu.png
Normal file
After Width: | Height: | Size: 170 KiB |
89
About/index.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
title: About me
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
status: Incomplete
|
||||||
|
about:
|
||||||
|
template: solana
|
||||||
|
image: Nicole_small.png
|
||||||
|
links:
|
||||||
|
- icon: github
|
||||||
|
text: Github
|
||||||
|
href: https://github.com/Drezil/
|
||||||
|
- icon: gitea
|
||||||
|
text: Gitea
|
||||||
|
href: https://gitea.dresselhaus.cloud
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work
|
||||||
|
|
||||||
|
- **March 2024 to September 2028**:
|
||||||
|
|
||||||
|
- Research-Software-Engineer at "Digital History" workgroup at
|
||||||
|
[HU Berlin](https://hu-berlin.de)
|
||||||
|
- Part of NFDI4Memory, Task Area 5 "Data Culture"
|
||||||
|
- PhD-ing on the side
|
||||||
|
|
||||||
|
- **January 2024 to February 2024**:
|
||||||
|
|
||||||
|
- Worked for [yarvis](https://yarvis.de/)
|
||||||
|
- First fulltime-developer; Responsive Webapp with react
|
||||||
|
|
||||||
|
- **March 2023 to September 2023**:
|
||||||
|
|
||||||
|
- Worked for [2lambda](http://2lambda.co)
|
||||||
|
- Silicon Valley start-up trying to beat the stock-market with fancy ML-Models
|
||||||
|
- That work kickstarted my employment at [Red Queen UG](https://red-queen.ug)
|
||||||
|
where i continue doing consulting work for [2lambda](http://2lambda.co)
|
||||||
|
while also moving into a more senior role of also building up our own team
|
||||||
|
of specialists to work on different future projects.
|
||||||
|
|
||||||
|
- **Oct. 2018 to Aug. 2021**:
|
||||||
|
|
||||||
|
- ML-Specialist at [Jobware](https://jobware.de) (Paderborn; german
|
||||||
|
Job-Advertising-Platform)
|
||||||
|
|
||||||
|
- **2013-2018** several jobs at my University including
|
||||||
|
- Worked 6 Months in the Workgroup "Theoretical Computer Science" on migrating
|
||||||
|
algorithms to **CUDA**
|
||||||
|
- Tutor "Introduction to Machine Learning"
|
||||||
|
- Was awarded **Tutoring-Award** of the Faculty of Technology for excellent
|
||||||
|
tutoring
|
||||||
|
- [Lecture "Intermediate Functional Programming in Haskell"](/Coding/Haskell/FFPiH.md)
|
||||||
|
- Development of Pandoc-Filters for effective **generation of lecture-slides**
|
||||||
|
for Mario Botsch (Leader Workgroup Computer Graphics) using Pandoc &
|
||||||
|
reveal.js
|
||||||
|
|
||||||
|
## Education
|
||||||
|
|
||||||
|
- **Bachelor** "Kognitive Informatik" (Cognitive Informatics) in Bielefeld
|
||||||
|
2010-2014
|
||||||
|
- **Master** "Naturwissenschaftliche Informatik" (Informatics in the natural
|
||||||
|
sciences) 2014-2018
|
||||||
|
|
||||||
|
### Extraordinary grades (Excerpt of my Transcript)
|
||||||
|
|
||||||
|
Note: Scale of grades in Germany is 1.0 to 4.0 with 1.0 being best, 4.0 being
|
||||||
|
passing grade, 5.0 being failed grade
|
||||||
|
|
||||||
|
- **1.0 in Modern Data Analysis**
|
||||||
|
- Master course on data-analysis (time-series, core-vector-machines, gaussian
|
||||||
|
processes, ...)
|
||||||
|
- **1.0 in Computergraphics**
|
||||||
|
- Raytracing, Modern OpenGL
|
||||||
|
- **1.3 in Computer-Animation**
|
||||||
|
- Dual-Quarternion-Skinning, Character-Animation, FACS-Poses, etc.
|
||||||
|
- **1.3 in GPU-Computing (CUDA)**
|
||||||
|
- originally a 1.7 by timing (task was de-mosaicing on images, grade was
|
||||||
|
measured in ms, whereby 400ms equated to 4.0 and 100ms equated to 1.0), but
|
||||||
|
because my deep knowledge was visible in the code i was given a 1.3 after
|
||||||
|
oral presentation.
|
||||||
|
- **1.0 in Parallel Algorithms and Data-Structures**
|
||||||
|
- **Ethical Hacking**
|
||||||
|
- Reverse Engineering with IDApro
|
||||||
|
|
||||||
|
## Further information
|
||||||
|
|
||||||
|
- [More details on my work-experience](Work.md)
|
||||||
|
- [More details of my coding](Experience.md)
|
||||||
|
- [More details of things i did beside studying at University](Extracurricular.md)
|
56
Coding/Haskell/Advantages.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
date: 2015-10-01
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Links
|
||||||
|
---
|
||||||
|
|
||||||
|
# Talks und Posts zu Haskell
|
||||||
|
|
||||||
|
Gründe Haskell zu nutzen und wo Vorteile liegen.
|
||||||
|
|
||||||
|
## Talks
|
||||||
|
|
||||||
|
- [The Future is parallel](https://www.youtube.com/watch?v=hlyQjK1qjw8)
|
||||||
|
- [Lenses](https://skillsmatter.com/skillscasts/4251-lenses-compositional-data-access-and-manipulation)
|
||||||
|
(Registrierung nötig - kostenfrei), siehe auch: [Lenses](Lenses.md)
|
||||||
|
- [Running a Startup on Haskell](https://www.youtube.com/watch?v=ZR3Jirqk6W8)
|
||||||
|
- [We're doing it all wrong](https://www.youtube.com/watch?v=TS1lpKBMkgg) - A
|
||||||
|
Long-Term Scala-Compiler-Developer quits his job after years and tells why
|
||||||
|
Scala is a mess.
|
||||||
|
- [Monads explained in Javascript](https://www.youtube.com/watch?v=b0EF0VTs9Dc)
|
||||||
|
- [Vinyl Records](http://vimeo.com/95694918) with
|
||||||
|
[Slides](https://github.com/VinylRecords/BayHac2014-Talk)
|
||||||
|
- [Thinking with Laziness](http://begriffs.com/posts/2015-06-17-thinking-with-laziness.html)
|
||||||
|
|
||||||
|
## Bücher/Paper
|
||||||
|
|
||||||
|
- [Papers on STM](https://research.microsoft.com/en-us/um/people/simonpj/papers/stm/)
|
||||||
|
- [Tackling the awkward squad](https://research.microsoft.com/en-us/um/people/simonpj/papers/marktoberdorf/)
|
||||||
|
- [Parallel and Concurrent Programming in Haskell](http://chimera.labs.oreilly.com/books/1230000000929/pr01.html)
|
||||||
|
- [Slides of a Quickcheck-Talk](http://scholar.google.de/scholar?cluster=7602244452224287116&hl=de&as_sdt=0,5)
|
||||||
|
- [Understanding F-Algebras](https://www.fpcomplete.com/user/bartosz/understanding-algebras)
|
||||||
|
schöne Erklärung. Man könnte danach anfangen den [Morphismen-zoo](Code
|
||||||
|
Snippets/Morphisms.md) zu verstehen...
|
||||||
|
- [Monad Transformers](https://github.com/kqr/gists/blob/master/articles/gentle-introduction-monad-transformers.md)
|
||||||
|
|
||||||
|
## Funny Talks
|
||||||
|
|
||||||
|
- [Tom LaGatta on Category-Theory](https://www.youtube.com/watch?v=o6L6XeNdd_k)
|
||||||
|
- [Unifying Structured Recursion Schemes](https://www.youtube.com/watch?v=9EGYSb9vov8)
|
||||||
|
aka. [[Morphisms|The Morphism-Zoo]]
|
||||||
|
- [Hole-Driven-Development Teaser (Enthusiasticon, raichoo)](https://www.youtube.com/watch?v=IRGKkiGG5CY)
|
||||||
|
|
||||||
|
## Unsorted/Unseen
|
||||||
|
|
||||||
|
- [Functional Reactive Programming](http://insights.pwning.de/_edit/Haskell/Why_Haskell_is_superior)
|
||||||
|
- [Diagrams: Declarative Vector Graphics in Haskell](http://vimeo.com/84104226)
|
||||||
|
- [Lenses, Folds & Traversels by Edward Kmett](https://www.youtube.com/watch?v=cefnmjtAolY)
|
||||||
|
- [Discrimination is Wrong by Edward Kmett](https://www.youtube.com/watch?v=cB8DapKQz-I&list=WL&index=10)
|
||||||
|
|
||||||
|
## Tutorials
|
||||||
|
|
||||||
|
- [Haskell fast and hard](https://www.fpcomplete.com/school/starting-with-haskell/haskell-fast-hard/haskell-fast-hard-part-1)
|
||||||
|
- [Counterexamples for Typeclasses](http://blog.functorial.com/posts/2015-12-06-Counterexamples.html)
|
203
Coding/Haskell/Code Snippets/Monoid.md
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
- Tutorial
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Tutorial
|
||||||
|
date: 2016-01-01
|
||||||
|
title: Monoid? Da war doch was...
|
||||||
|
abstract: |
|
||||||
|
Monoide tauchen überall auf. Ein Grund sich damit mal etwas eingehender an einen konkreten Beispiel zu beschäftigen.
|
||||||
|
---
|
||||||
|
|
||||||
|
Stellen wir uns vor, dass wir eine Funktion schreiben, die einen String bekommt
|
||||||
|
(mehrere Lines mit ACSII-Text) und dieses Wort-für-Wort rückwärts ausgeben soll.
|
||||||
|
Das ist ein einfacher Einzeiler:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import System.Environment (getArgs)
|
||||||
|
import Data.Monoid (mconcat)
|
||||||
|
import Data.Functor ((<$>))
|
||||||
|
|
||||||
|
main = do
|
||||||
|
ls <- readFile =<< head <$> getArgs
|
||||||
|
mconcat <$> mapM (putStrLn . unwords . reverse . words) (lines ls) --die eigentliche Funktion, ls ist das argument.
|
||||||
|
```
|
||||||
|
|
||||||
|
Was passiert hier an Vodoo? Und was machen die ganzen wilden Zeichen da?
|
||||||
|
|
||||||
|
Gehen wir die Main zeilenweise durch: Wir lesen die Datei, die im ersten
|
||||||
|
Kommandozeilen-Argument gegeben wird. getArgs hat folgende Signatur:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
getArgs :: IO [String]
|
||||||
|
```
|
||||||
|
|
||||||
|
Wir bekommen als eine Liste der Argumente. Wir wollen nur das erste. Also machen
|
||||||
|
wir head getArgs. Allerdings fliegt uns dann ein Fehler. head sieht nämlich so
|
||||||
|
aus:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
head :: [a] -> a
|
||||||
|
```
|
||||||
|
|
||||||
|
Irgendwie müssen wird as **in** das IO bekommen. Hierzu gibt es fmap. Somit ist
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
fmap head :: IO [a] -> IO a
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein inline-Alias (um die Funktion links und das Argument rechts zu schreiben und
|
||||||
|
sich ne Menge Klammern zu sparen) ist <$>. Somit ist schlussendlich der Inhalt
|
||||||
|
der Datei aus dem ersten Argument (lazy) in ls.
|
||||||
|
|
||||||
|
Eine andere Möglichkeit sich das (in diesem Fall) zu merken, bzw. drauf zu
|
||||||
|
kommen ist, dass [] AUCH ein Funktor (sogar eine Monade) ist. Man könnte das
|
||||||
|
also auch so schreiben:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
head :: [] a -> a
|
||||||
|
head :: Functor f => [] (f a) -> f a -- das "a" geschickt ersetzt zur Verdeutlichung
|
||||||
|
getArgs :: IO [] String
|
||||||
|
fmap head :: Functor f => f [] a -> f a
|
||||||
|
```
|
||||||
|
|
||||||
|
fmap "packt" die Funktion quasi 1 Umgebung (Funktor, Monade, ..) weiter rein -
|
||||||
|
Sei es nun in Maybe, Either oder irgendwas anderes.
|
||||||
|
|
||||||
|
Alternatives (ausführliches) Beispiel am Ende.
|
||||||
|
|
||||||
|
Wenn wir uns die Signatur ansehen, dann haben wir nun
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
head <$> getArgs :: IO String
|
||||||
|
```
|
||||||
|
|
||||||
|
readFile will aber nun ein String haben. Man kann nun
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f <- head <$> getArgs
|
||||||
|
ls <- readFile f
|
||||||
|
```
|
||||||
|
|
||||||
|
kann man auch "inline" mit =<< die Sachen "auspacken".
|
||||||
|
|
||||||
|
Die 2. Zeile lesen wir nun einfach "von hinten", wie man das meistens tun
|
||||||
|
sollte. Hier ist ein
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
lines ls :: [String]
|
||||||
|
```
|
||||||
|
|
||||||
|
was uns den Inhalt der Datei zeilenweise gibt. Mit jeder Zeile möchten wir nun
|
||||||
|
folgendes machen:
|
||||||
|
|
||||||
|
1. nach Wörtern trennen (words)
|
||||||
|
2. Wörter in der reihenfolge umkehren (reverse)
|
||||||
|
3. Wörter wider zu einer Zeile zusammensetzen (unwords)
|
||||||
|
4. diese Zeile ausgeben (putStrLn)
|
||||||
|
|
||||||
|
Wenn wir uns die Signatur ansehen:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
(putStrLn . unwords . reverse . words) :: String -> IO ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Das mag im ersten Moment verwirren, daher noch die Signaturen der
|
||||||
|
Einzelfunktionen:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
words :: String -> [String]
|
||||||
|
reverse :: [a] -> [a]
|
||||||
|
unwords :: [String] -> String
|
||||||
|
putStrLn :: String -> IO ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Da wir am Ende in der IO-Monade landen müssen wir das auf unsere Zeilen mit mapM
|
||||||
|
statt map anwenden. Dies sorgt auch dafür, dass die Liste der reihe nach
|
||||||
|
durchgegangen wird. mapM mit unserer Funktion schaut dann so aus:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mapM (putStrLn . unwords . reverse . words) :: [String] -> [IO ()]
|
||||||
|
```
|
||||||
|
|
||||||
|
eek! Das [IO ()] sieht ekelig aus. Wir haben eine Liste von IO-gar nichts. Das
|
||||||
|
können wir eigentlich entsorgen. Da wir innerhalb der main-Funktion in einer
|
||||||
|
IO-Monade sind, wollen wir IO () anstatt [IO ()] zurück haben.
|
||||||
|
|
||||||
|
Wenn wir uns jetzt erinnern, dass [] auch nur eine Monade ist und dass jede
|
||||||
|
Monade ein Monoid ist, dann ist die Lösung einfach. Monoide haben eine
|
||||||
|
"append"-funktion (mappend oder (<>) genannt). Wenn wir "nichts" an "nichts"
|
||||||
|
anhängen, dann erhalten wir .... _Trommelwirbel_ "nichts"! Wir müssen die [IO
|
||||||
|
()]-Liste also "nur noch" mit mappend falten. Hierzu gibt es schon eine
|
||||||
|
vorgefertigte Funktion:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mconcat :: [a] -> a
|
||||||
|
mconcat = foldr mappend mempty
|
||||||
|
```
|
||||||
|
|
||||||
|
Was genau die gewünschte Faltung macht. Wir müssen nun wieder fmap nehmen, da
|
||||||
|
wir die Liste selbst falten wollen - und nicht map, welches auf den IO ()
|
||||||
|
innerhalb der Liste arbeiten würde. Durch die Faltung fällt die Liste nun auf IO
|
||||||
|
() zusammen.
|
||||||
|
|
||||||
|
Viel Voodoo in wenig Code, aber wenn man sich dran gewöhnt hat, sind Monaden in
|
||||||
|
Monaden auch nicht schlimm. Man muss sich immer nur richtig "rein" fmap'en.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Kleinen Tipp gab es noch: mapM\_ macht genau das, was oben mit mconcat erreicht
|
||||||
|
werden sollte. Somit kann man auch
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mapM_ (putStrLn . unwords . reverse . words) (lines ls)
|
||||||
|
```
|
||||||
|
|
||||||
|
schreiben. Ich hab es aber mal wegen der klarheit oben so gelassen.
|
||||||
|
|
||||||
|
## Alternatives fmap-Beispiel
|
||||||
|
|
||||||
|
Nehmen wir als alternatives Beispiel mal an:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
a :: IO Maybe State t
|
||||||
|
```
|
||||||
|
|
||||||
|
Um Funktionen vom Typ
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f :: IO a -> IO a
|
||||||
|
f a -- valide
|
||||||
|
```
|
||||||
|
|
||||||
|
zu nehmen, brauchen wir nichts machen. Bei
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f' :: Maybe a -> Maybe a
|
||||||
|
```
|
||||||
|
|
||||||
|
brauchen wir 1 fmap, also ein
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f' a -- error
|
||||||
|
f' <$> a
|
||||||
|
```
|
||||||
|
|
||||||
|
um eine Funktion
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f'' :: State t -> State t
|
||||||
|
```
|
||||||
|
|
||||||
|
zu benutzen folglich:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
f'' a -- error
|
||||||
|
f'' <$> a -- error
|
||||||
|
fmap f'' <$> a
|
||||||
|
```
|
259
Coding/Haskell/Code Snippets/Morphisms.md
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
- Tutorial
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Tutorial
|
||||||
|
- Archived
|
||||||
|
title: "*-Morpisms"
|
||||||
|
date: 2016-01-01
|
||||||
|
abstract: |
|
||||||
|
This weekend I spend some time on Morphisms.
|
||||||
|
|
||||||
|
Knowing that this might sound daunting to many dabbling Haskellers (like I am), I decided to write a real short MergeSort hylomorphism quickstarter.
|
||||||
|
---
|
||||||
|
|
||||||
|
::: {.callout-note}
|
||||||
|
|
||||||
|
Backup eines Blogposts eines Kommilitonen
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
This weekend I spend some time on Morphisms.
|
||||||
|
|
||||||
|
Knowing that this might sound daunting to many dabbling Haskellers (like I am),
|
||||||
|
I decided to write a real short MergeSort hylomorphism quickstarter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For those who need a refresher: MergeSort works by creating a balanced binary
|
||||||
|
tree from the input list and directly collapsing it back into itself while
|
||||||
|
treating the children as sorted lists and merging these with an O(n) algorithm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
First the usual prelude:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# LANGUAGE DeriveFunctor #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
|
import Data.Functor.Foldable
|
||||||
|
import Data.List (splitAt, unfoldr)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We will use a binary tree like this. Note that there is no explicit recursion
|
||||||
|
used, but `NodeF` has two _holes_. These will eventually filled later.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
data TreeF c f = EmptyF | LeafF c | NodeF f f
|
||||||
|
deriving (Eq, Show, Functor)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Aside: We could use this as a _normal_ binary tree by wrapping it in `Fix`:
|
||||||
|
`type Tree a = Fix (TreeF a)` But this would require us to write our tree like
|
||||||
|
`Fix (NodeF (Fix (LeafF 'l')) (Fix (LeafF 'r')))` which would get tedious fast.
|
||||||
|
Luckily Edward build a much better way to do this into _recursion-schemes_. I
|
||||||
|
will touch on this later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Without further ado we start to write a Coalgebra, which in my book is just a
|
||||||
|
scary name for "function that is used to construct datastructures".
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
unflatten :: [a] -> TreeF a [a]
|
||||||
|
unflatten ( []) = EmptyF
|
||||||
|
unflatten (x:[]) = LeafF x
|
||||||
|
unflatten ( xs) = NodeF l r where (l,r) = splitAt (length xs `div` 2) xs
|
||||||
|
```
|
||||||
|
|
||||||
|
From the type signature it's immediately obvious, that we take a list of 'a's
|
||||||
|
and use it to create a part of our tree.
|
||||||
|
|
||||||
|
The nice thing is that due to the fact that we haven't commited to a type in our
|
||||||
|
tree nodes we can just put lists in there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Aside: At this point we could use this Coalgebra to construct (unsorted) binary
|
||||||
|
trees from lists:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
example1 = ana unflatten [1,3] == Fix (NodeF (Fix (LeafF 1)) (Fix (LeafF 3)))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
On to our sorting, tree-collapsing Algebra. Which again is just a creepy word
|
||||||
|
for "function that is used to deconstruct datastructures".
|
||||||
|
|
||||||
|
The function `mergeList` is defined below and just merges two sorted lists into
|
||||||
|
one sorted list in O(n), I would probably take this from the `ordlist` package
|
||||||
|
if I were to implement this _for real_.
|
||||||
|
|
||||||
|
Again we see that we can just construct our sorted output list from a `TreeF`
|
||||||
|
that apparently contains just lists.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
flatten :: Ord a => TreeF a [a] -> [a]
|
||||||
|
flatten EmptyF = []
|
||||||
|
flatten (LeafF c) = [c]
|
||||||
|
flatten (NodeF l r) = mergeLists l r
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Aside: We could use a Coalgebra to deconstruct trees:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
example2 = cata flatten (Fix (NodeF (Fix (LeafF 3)) (Fix (LeafF 1)))) == [1,3]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Now we just combine the Coalgebra and the Algebra with one from the functions
|
||||||
|
from Edwards `recursion-schemes` library:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mergeSort :: Ord a => [a] -> [a]
|
||||||
|
mergeSort = hylo flatten unflatten
|
||||||
|
|
||||||
|
example3 = mergeSort [5,2,7,9,1,4] == [1,2,4,5,7,9]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
What have we gained?
|
||||||
|
|
||||||
|
We have implemented a MergeSort variant in 9 lines of code, not counting the
|
||||||
|
`mergeLists` function below. Not bad, but
|
||||||
|
[this implementation](<http://en.literateprograms.org/Merge_sort_(Haskell)>) is
|
||||||
|
not much longer.
|
||||||
|
|
||||||
|
On the other hand the morphism based implementation cleanly describes what
|
||||||
|
happens during construction and deconstruction of our intermediate structure.
|
||||||
|
|
||||||
|
My guess is that, as soon as the algortihms get more complex, this will really
|
||||||
|
make a difference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
At this point I wasn't sure if this was useful or remotely applicable. Telling
|
||||||
|
someone "I spend a whole weekend learning about Hylomorphism" isn't something
|
||||||
|
the cool developer kids do.
|
||||||
|
|
||||||
|
It appeared to me that maybe I should have a look at the Core to see what the
|
||||||
|
compiler finally comes up with (edited for brevity):
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mergeSort :: [Integer] -> [Integer]
|
||||||
|
mergeSort =
|
||||||
|
\ (x :: [Integer]) ->
|
||||||
|
case x of wild {
|
||||||
|
[] -> [];
|
||||||
|
: x1 ds ->
|
||||||
|
case ds of _ {
|
||||||
|
[] -> : x1 ([]);
|
||||||
|
: ipv ipv1 ->
|
||||||
|
unfoldr
|
||||||
|
lvl9
|
||||||
|
(let {
|
||||||
|
p :: ([Integer], [Integer])
|
||||||
|
p =
|
||||||
|
case $wlenAcc wild 0 of ww { __DEFAULT ->
|
||||||
|
case divInt# ww 2 of ww4 { __DEFAULT ->
|
||||||
|
case tagToEnum# (<# ww4 0) of _ {
|
||||||
|
False ->
|
||||||
|
case $wsplitAt# ww4 wild of _ { (# ww2, ww3 #) -> (ww2, ww3) };
|
||||||
|
True -> ([], wild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} } in
|
||||||
|
(case p of _ { (x2, ds1) -> mergeSort x2 },
|
||||||
|
case p of _ { (ds1, y) -> mergeSort y }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end Rec }
|
||||||
|
```
|
||||||
|
|
||||||
|
While I am not really competent in reading Core and this is actually the first
|
||||||
|
time I bothered to try, it is immediately obvious that there is no trace of any
|
||||||
|
intermediate tree structure.
|
||||||
|
|
||||||
|
This is when it struck me. I was dazzled and amazed. And am still. Although we
|
||||||
|
are writing our algorithm as if we are working on a real tree structure the
|
||||||
|
library and the compiler are able to just remove the whole intermediate step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Aftermath:
|
||||||
|
|
||||||
|
In the beginning I promised a way to work on non-functor data structures.
|
||||||
|
Actually that was how I began to work with the `recursion-schemes` library.
|
||||||
|
|
||||||
|
We are able to create a 'normal' version of our tree from above:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
data Tree c = Empty | Leaf c | Node (Tree c) (Tree c)
|
||||||
|
deriving (Eq, Show)
|
||||||
|
```
|
||||||
|
|
||||||
|
But we can not use this directly with our (Co-)Algebras. Luckily Edward build a
|
||||||
|
little bit of type magic into the library:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
type instance Base (Tree c) = (TreeF c)
|
||||||
|
|
||||||
|
instance Unfoldable (Tree c) where
|
||||||
|
embed EmptyF = Empty
|
||||||
|
embed (LeafF c) = Leaf c
|
||||||
|
embed (NodeF l r) = Node l r
|
||||||
|
|
||||||
|
instance Foldable (Tree c) where
|
||||||
|
project Empty = EmptyF
|
||||||
|
project (Leaf c) = LeafF c
|
||||||
|
project (Node l r) = NodeF l r
|
||||||
|
```
|
||||||
|
|
||||||
|
Without going into detail by doing this we establish a relationship between
|
||||||
|
`Tree` and `TreeF` and teach the compiler how to translate between these types.
|
||||||
|
|
||||||
|
Now we can use our Alebra on our non functor type:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
example4 = cata flatten (Node (Leaf 'l') (Leaf 'r')) == "lr"
|
||||||
|
```
|
||||||
|
|
||||||
|
The great thing about this is that, looking at the Core output again, there is
|
||||||
|
no traces of the `TreeF` structure to be found. As far as I can tell, the
|
||||||
|
algorithm is working directly on our `Tree` type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Literature:
|
||||||
|
|
||||||
|
- [Understanding F-Algebras](https://www.fpcomplete.com/user/bartosz/understanding-algebras)
|
||||||
|
- [Recursion Schemes by Example](http://www.timphilipwilliams.com/slides.html)
|
||||||
|
- [Recursion Schemes: A Field Guide](http://comonad.com/reader/2009/recursion-schemes/)
|
||||||
|
- [This StackOverflow question](http://stackoverflow.com/questions/6941904/recursion-schemes-for-dummies)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Appendix:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
mergeLists :: Ord a => [a] -> [a] -> [a]
|
||||||
|
mergeLists = curry $ unfoldr c where
|
||||||
|
c ([], []) = Nothing
|
||||||
|
c ([], y:ys) = Just (y, ([], ys))
|
||||||
|
c (x:xs, []) = Just (x, (xs, []))
|
||||||
|
c (x:xs, y:ys) | x <= y = Just (x, (xs, y:ys))
|
||||||
|
| x > y = Just (y, (x:xs, ys))
|
||||||
|
```
|
74
Coding/Haskell/FFPiH.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
- Experience
|
||||||
|
categories:
|
||||||
|
- Lecture
|
||||||
|
- Haskell
|
||||||
|
date: 2018-01-01
|
||||||
|
title: Fortgeschrittene funktionale Programmierung in Haskell
|
||||||
|
---
|
||||||
|
|
||||||
|
FFPiH ist eine Vorlesung, die ich zusammen mit einem Kommilitonen im Sommer 2015
|
||||||
|
erstmals erstellt und gehalten haben.
|
||||||
|
|
||||||
|
Insgesamt haben wir die Vorlesung 3x gehalten, wobei von der ersten zur zweiten
|
||||||
|
Iteration der Inhalt massiv überarbeitet wurde und bei der Iteration von der
|
||||||
|
zweiten zur dritten Vorlesung die Übungen komplett neu erstellt wurden.
|
||||||
|
|
||||||
|
Die gesamten Übungen sind unter anderem in der FFPiH-Organisation in meinem
|
||||||
|
gitea hinterlegt:
|
||||||
|
[https://gitea.dresselhaus.cloud/FFPiH](https://gitea.dresselhaus.cloud/FFPiH)
|
||||||
|
|
||||||
|
Einige der aktualisierten Übungen sind privat geschaltet, da diese iterativ
|
||||||
|
aufeinander aufbauen und jeweils die Musterlösung der vorherigen enthalten.
|
||||||
|
|
||||||
|
## Aufbau der Vorlesung
|
||||||
|
|
||||||
|
Vorausgesetzt wurde, dass die Studierenden das erste Semester abgeschlossen
|
||||||
|
hatten und somit bereits leichte Grundlagen in Haskell kannten (aber z.b. Dinge
|
||||||
|
wie Functor/Applicative/Monad noch nicht _wirklich_ erklärt bekommen haben).
|
||||||
|
|
||||||
|
Stück für Stück werden die Studis dann zunächst in abstrakte Konstrukte
|
||||||
|
eingeführt, aber diese werden dann schnell in die Praxis umgesetzt. Etwa mit dem
|
||||||
|
Schreiben eines eigenen Parsers.
|
||||||
|
|
||||||
|
Schlussendlich gibt es dann einen "Rundumschlag" durch die gesamte Informatik.
|
||||||
|
Erstellung eines Spieles (auf basis einer kleinen Grundlage), erstellung von
|
||||||
|
WebApps mit Yesod, Parallelisierung und Nebenläufigkeit für rechenintensive
|
||||||
|
Anwendungen inkl. synchronisation mittels STM.
|
||||||
|
|
||||||
|
Optional gab es weitere Übungen zu dingen wie "verteiltes Rechnen".
|
||||||
|
|
||||||
|
Ziel hierbei war nicht, diese ganzen Themen in der Tiefe beizubringen, sondern
|
||||||
|
aufzuzeigen, wie sie sehr schnell abstrakte Konstrukte, die ihnen ggf. 3
|
||||||
|
Semester später erst begegnen bugfrei benutzen können, da Haskell hier in sehr
|
||||||
|
vielen Fällen einfach nur die "richtige" Lösung kompilieren lässt und alle
|
||||||
|
gängigen Fallen schlicht ausschließt. Beispiel ist z.b. STM innerhalb von STM,
|
||||||
|
Mischen von DB-Monade, Handler-Monade und Template-Engine in Yesod, Process ()
|
||||||
|
statt IO () in der Nutzung von CloudHaskell, etc. pp.
|
||||||
|
|
||||||
|
## Studentisches Feedback
|
||||||
|
|
||||||
|
Sehr gutes Feedback von den Studenten bekamen wir insbesondere für Übungen wie:
|
||||||
|
|
||||||
|
[Übung 2, Aufgabe 2](https://gitea.dresselhaus.cloud/FFPiH/uebung2017_2/src/branch/master/src/Aufgabe2.hs),
|
||||||
|
weil hier durch "einfaches" umformen hin zu Abstraktionen und mit den Regeln
|
||||||
|
dieser im ersten Fall die Laufzeit (vor Compileroptimierungen) von O(n²) auf
|
||||||
|
O(0) ändert.
|
||||||
|
|
||||||
|
[Übung 4](https://gitea.dresselhaus.cloud/FFPiH/uebung2017-4), welche ein
|
||||||
|
komplett fertigen (sehr rudimentären und simplen) Dungeon-Crawler bereitstellt,
|
||||||
|
der "nur" 1-2 bugs hat und "wie ein echtes Projekt" erweitert werden muss. Diese
|
||||||
|
Übung hat sich dann über 4 weitere Übungen gestreckt, wobei folgende Aufgaben
|
||||||
|
gelöst werden müssen:
|
||||||
|
|
||||||
|
- Einarbeitung in QuickCheck zur Behebung eines Bugs im Test
|
||||||
|
- Umschreiben von explizitem Argument-Passing hin zu Monad-Transformers mit
|
||||||
|
stateful [Lenses](Lenses.md)
|
||||||
|
- Continuation-Basierendes Event-System
|
||||||
|
- Hinzufügen eines Parsers für Level, Items & deren Effekte und implementation
|
||||||
|
dieser
|
||||||
|
- Ändern des GUI-Parts von CLI auf 2D GL mittels gloss
|
||||||
|
- Ändern von `StateT World` auf `RWST GameConfig Log World` und somit nutzen von
|
||||||
|
individuellen Konfigurationen für z.b. Keybindings
|
580
Coding/Haskell/Lenses.md
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
title: Lenses
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- Haskell
|
||||||
|
date: "2018-01-01"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wofür brauchen wir das überhaupt?
|
||||||
|
|
||||||
|
Die Idee dahinter ist, dass man Zugriffsabstraktionen über Daten verknüpfen
|
||||||
|
kann. Also einfachen Datenstruktur kann man einen Record mit der
|
||||||
|
entsprechenden
|
||||||
|
Syntax nehmen.
|
||||||
|
|
||||||
|
### Beispiel
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Person = P { name :: String
|
||||||
|
, addr :: Address
|
||||||
|
, salary :: Int }
|
||||||
|
data Address = A { road :: String
|
||||||
|
, city :: String
|
||||||
|
, postcode :: String }
|
||||||
|
-- autogeneriert unten anderem: addr :: Person -> Address
|
||||||
|
|
||||||
|
setName :: String -> Person -> Person
|
||||||
|
setName n p = p { name = n } --record update notation
|
||||||
|
|
||||||
|
setPostcode :: String -> Person -> Person
|
||||||
|
setPostcode pc p
|
||||||
|
= p { addr = addr p { postcode = pc } }
|
||||||
|
-- update of a record inside a record
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Problem mit diesem Code:
|
||||||
|
|
||||||
|
- für 1-Dimensionale Felder ist die record-syntax ok.
|
||||||
|
- tiefere Ebenen nur umständlich zu erreichen
|
||||||
|
- eigentlich wollen wir nur pe in p setzen, müssen aber über addr etc. gehen.
|
||||||
|
- wir brauchen wissen über die "Zwischenstrukturen", an denen wir nicht
|
||||||
|
interessiert sind
|
||||||
|
|
||||||
|
### Was wir gern hätten
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Person = P { name :: String
|
||||||
|
, addr :: Address
|
||||||
|
, salary :: Int }
|
||||||
|
-- a lens for each field
|
||||||
|
lname :: Lens' Person String
|
||||||
|
laddr :: Lens' Person Adress
|
||||||
|
lsalary :: Lens' Person Int
|
||||||
|
-- getter/setter for them
|
||||||
|
view :: Lens' s a -> s -> a
|
||||||
|
set :: Lens' s a -> a -> s -> s
|
||||||
|
-- lens-composition
|
||||||
|
composeL :: Lens' s1 s2 -> Lens s2 a -> Lens' s1 a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wie uns das hilft
|
||||||
|
|
||||||
|
Mit diesen Dingen (wenn wir sie hätten) könnte man dann
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Person = P { name :: String
|
||||||
|
, addr :: Address
|
||||||
|
, salary :: Int }
|
||||||
|
data Address = A { road :: String
|
||||||
|
, city :: String
|
||||||
|
, postcode :: String }
|
||||||
|
setPostcode :: String -> Person -> Person
|
||||||
|
setPostcode pc p
|
||||||
|
= set (laddr `composeL` lpostcode) pc p
|
||||||
|
```
|
||||||
|
|
||||||
|
machen und wäre fertig.
|
||||||
|
|
||||||
|
## Trivialer Ansatz
|
||||||
|
|
||||||
|
### Getter/Setter also Lens-Methoden
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data LensR s a = L { viewR :: s -> a
|
||||||
|
, setR :: a -> s -> s }
|
||||||
|
|
||||||
|
composeL (L v1 u1) (L v2 u2)
|
||||||
|
= L (\s -> v2 (v1 s))
|
||||||
|
(\a s -> u1 (u2 a (v1 s)) s)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wieso ist das schlecht?
|
||||||
|
|
||||||
|
- extrem ineffizient
|
||||||
|
|
||||||
|
Auslesen traversiert die Datenstruktur, dann wird die Function angewendet und
|
||||||
|
zum setzen wird die Datenstruktur erneut traversiert:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
over :: LensR s a -> (a -> a) -> s -> s
|
||||||
|
over ln f s = setR l (f (viewR l s)) s
|
||||||
|
```
|
||||||
|
|
||||||
|
- Lösung: modify-funktion hinzufügen
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data LensR s a
|
||||||
|
= L { viewR :: s -> a
|
||||||
|
, setR :: a -> s -> s
|
||||||
|
, mod :: (a->a) -> s -> s
|
||||||
|
, modM :: (a->Maybe a) -> s -> Maybe s
|
||||||
|
, modIO :: (a->IO a) -> s -> IO s }
|
||||||
|
```
|
||||||
|
|
||||||
|
Neues Problem: Für jeden Spezialfall muss die Lens erweitert werden.
|
||||||
|
|
||||||
|
### Something in common
|
||||||
|
|
||||||
|
Man kann alle Monaden abstrahieren. Functor reicht schon:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data LensR s a
|
||||||
|
= L { viewR :: s -> a
|
||||||
|
, setR :: a -> s -> s
|
||||||
|
, mod :: (a->a) -> s -> s
|
||||||
|
, modF :: Functor f => (a->f a) -> s -> f s }
|
||||||
|
```
|
||||||
|
|
||||||
|
Idee: Die 3 darüberliegenden durch modF ausdrücken.
|
||||||
|
|
||||||
|
### Typ einer Lens
|
||||||
|
|
||||||
|
Wenn man das berücksichtigt, dann hat einen Lens folgenden Typ:
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
type Lens' s a = forall f. Functor f
|
||||||
|
=> (a -> f a) -> s -> f s
|
||||||
|
```
|
||||||
|
|
||||||
|
Allerdings haben wir dann noch unseren getter/setter:
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
data LensR s a = L { viewR :: s -> a
|
||||||
|
, setR :: a -> s -> s }
|
||||||
|
```
|
||||||
|
|
||||||
|
Stellt sich raus: Die sind isomorph! Auch wenn die von den Typen her komplett
|
||||||
|
anders aussehen.
|
||||||
|
|
||||||
|
## Benutzen einer Lens also Setter
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
set :: Lens' s a -> (a -> s -> s)
|
||||||
|
set ln a s = --...umm...
|
||||||
|
--:t ln => (a -> f a) -> s -> f s
|
||||||
|
-- => get s out of f s to return it
|
||||||
|
```
|
||||||
|
|
||||||
|
Wir können für f einfach die "Identity"-Monade nehmen, die wir nachher
|
||||||
|
wegcasten
|
||||||
|
können.
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
newtype Identity a = Identity a
|
||||||
|
-- Id :: a -> Identity a
|
||||||
|
|
||||||
|
runIdentity :: Identity s -> s
|
||||||
|
runIdentity (Identity x) = x
|
||||||
|
|
||||||
|
instance Functor Identity where
|
||||||
|
fmap f (Identity x) = Identity (f x)
|
||||||
|
```
|
||||||
|
|
||||||
|
somit ist set einfach nur
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
set :: Lens' s a -> (a -> s -> s)
|
||||||
|
set ln x s
|
||||||
|
= runIdentity (ls set_fld s)
|
||||||
|
where
|
||||||
|
set_fld :: a -> Identity a
|
||||||
|
set_fld _ = Identity x
|
||||||
|
-- a was the OLD value.
|
||||||
|
-- We throw that away and set the new value
|
||||||
|
```
|
||||||
|
|
||||||
|
oder kürzer (für nerds wie den Author der Lens-Lib)
|
||||||
|
|
||||||
|
```{.haskell }
|
||||||
|
set :: Lens' s a -> (a -> s -> s)
|
||||||
|
set ln x = runIdentity . ln (Identity . const x)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benutzen einer Lens also Modify
|
||||||
|
|
||||||
|
Dasselbe wie Set, nur dass wir den Parameter nicht entsorgen, sondern in die
|
||||||
|
mitgelieferte Function stopfen.
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
over :: Lens' s a -> (a -> a) -> s -> s
|
||||||
|
over ln f = runIdentity . ln (Identity . f)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benutzen einer Lens also Getter
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
view :: Lens' s a -> (s -> a)
|
||||||
|
view ln s = --...umm...
|
||||||
|
--:t ln => (a -> f a) -> s -> f s
|
||||||
|
-- => get a out of the (f s) return-value
|
||||||
|
-- Wait, WHAT?
|
||||||
|
```
|
||||||
|
|
||||||
|
Auch hier gibt es einen netten Funktor. Wir packen das "a" einfach in das "f"
|
||||||
|
und werfen das "s" am End weg.
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
newtype Const v a = Const v
|
||||||
|
|
||||||
|
getConst :: Const v a -> v
|
||||||
|
getConst (Const x) = x
|
||||||
|
|
||||||
|
instance Functor (Const v) where
|
||||||
|
fmap f (Const x) = Const x
|
||||||
|
-- throw f away. Nothing changes our const!
|
||||||
|
```
|
||||||
|
|
||||||
|
somit ergibt sich
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
view :: Lens' s a -> (s -> a)
|
||||||
|
view ln s
|
||||||
|
= getConst (ln Const s)
|
||||||
|
-- Const :: s -> Const a s
|
||||||
|
```
|
||||||
|
|
||||||
|
oder nerdig
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
view :: Lens' s a -> (s -> a)
|
||||||
|
view ln = getConst . ln Const
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lenses bauen
|
||||||
|
|
||||||
|
Nochmal kurz der Typ:
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
type Lens' s a = forall f. Functor f
|
||||||
|
=> (a -> f a) -> s -> f s
|
||||||
|
```
|
||||||
|
|
||||||
|
Für unser Personen-Beispiel vom Anfang:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Person = P { _name :: String, _salary :: Int }
|
||||||
|
|
||||||
|
name :: Lens' Person String
|
||||||
|
-- name :: Functor f => (String -> f String)
|
||||||
|
-- -> Person -> f Person
|
||||||
|
|
||||||
|
name elt_fn (P n s)
|
||||||
|
= fmap (\n' -> P n' s) (elt_fn n)
|
||||||
|
-- fmap :: Functor f => (a->b) -> f a -> f b - der Funktor, der alles verknüpft
|
||||||
|
-- \n' -> .. :: String -> Person - Funktion um das Element zu lokalisieren (WO wird ersetzt/gelesen/...)
|
||||||
|
-- elt_fn n :: f String - Funktion um das Element zu verändern (setzen, ändern, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Lambda-Funktion ersetzt einfach den Namen. Häufig sieht man auch
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
name elt_fn (P n s)
|
||||||
|
= (\n' -> P n' s) <$> (elt_fn n)
|
||||||
|
-- | Focus | |Function|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wie funktioniert das intern?
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
view name (P {_name="Fred", _salary=100})
|
||||||
|
-- inline view-function
|
||||||
|
= getConst (name Const (P {_name="Fred", _salary=100})
|
||||||
|
-- inline name
|
||||||
|
= getConst (fmap (\n' -> P n' 100) (Const "Fred"))
|
||||||
|
-- fmap f (Const x) = Const x - Definition von Const
|
||||||
|
= getConst (Const "Fred")
|
||||||
|
-- getConst (Const x) = x
|
||||||
|
= "Fred"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Aufruf hat KEINE Runtime-Kosten, weil der Compiler direkt die Address
|
||||||
|
des
|
||||||
|
Feldes einsetzen kann. Der gesamte Boilerplate-Code wird vom Compiler
|
||||||
|
wegoptimiert.
|
||||||
|
|
||||||
|
Dies gilt für jeden Funktor mit newtype, da das nur ein Typalias ist.
|
||||||
|
|
||||||
|
## Composing Lenses und deren Benutzung
|
||||||
|
|
||||||
|
Wie sehen denn die Typen aus?
|
||||||
|
|
||||||
|
Wir wollen ein
|
||||||
|
|
||||||
|
> Lens' s1 s2 -> Lens' s2 a -> Lens' s1 a
|
||||||
|
|
||||||
|
Wir haben 2 Lenses
|
||||||
|
|
||||||
|
> ln1 :: (s2 -> f s2) -> (s1 -> f s1)
|
||||||
|
> ln2 :: (a -> f a) -> (s2 -> f s2)
|
||||||
|
|
||||||
|
wenn man scharf hinsieht, kann man die verbinden
|
||||||
|
|
||||||
|
> ln1 . ln2 :: (a -> f s) -> (s1 -> f s1)
|
||||||
|
|
||||||
|
und erhält eine Lens. Sogar die Gewünschte!
|
||||||
|
Somit ist Lens-Composition einfach nur Function-Composition (.).
|
||||||
|
|
||||||
|
## Automatisieren mit Template-Haskell
|
||||||
|
|
||||||
|
Der Code um die Lenses zu bauen ist für records immer Identisch:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Person = P { _name :: String, _salary :: Int }
|
||||||
|
|
||||||
|
name :: Lens' Person String
|
||||||
|
name elt_fn (P n s) = (\n' -> P n' s) <$> (elt_fn n)
|
||||||
|
```
|
||||||
|
|
||||||
|
Daher kann man einfach
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
import Control.Lens.TH
|
||||||
|
data Person = P { _name :: String, _salary :: Int }
|
||||||
|
|
||||||
|
$(makeLenses ''Person)
|
||||||
|
```
|
||||||
|
|
||||||
|
nehmen, was einem eine Lens für "name" und eine Lens für "salary" generiert.
|
||||||
|
Mit anderen Templates kann man auch weitere Dinge steuern (etwa wofür Lenses
|
||||||
|
generiert werden, welches Prefix (statt \_) man haben will etc. pp.).
|
||||||
|
|
||||||
|
Will man das aber haben, muss man selbst in den Control.Lens.TH-Code schauen.
|
||||||
|
|
||||||
|
## Lenses für den Beispielcode
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
import Control.Lens.TH
|
||||||
|
|
||||||
|
data Person = P { _name :: String
|
||||||
|
, _addr :: Address
|
||||||
|
, _salary :: Int }
|
||||||
|
data Address = A { _road :: String
|
||||||
|
, _city :: String
|
||||||
|
, _postcode :: String }
|
||||||
|
|
||||||
|
$(makeLenses ''Person)
|
||||||
|
$(makeLenses ''Address)
|
||||||
|
|
||||||
|
setPostcode :: String -> Person -> Person
|
||||||
|
setPostcode pc p = set (addr . postcode) pc p
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shortcuts mit "Line-Noise"
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
-- ...
|
||||||
|
|
||||||
|
setPostcode :: String -> Person -> Person
|
||||||
|
setPostcode pc p = addr . postcode .~ pc $ p
|
||||||
|
-- | Focus |set|to what|in where
|
||||||
|
|
||||||
|
getPostcode :: Person -> String
|
||||||
|
getPostcode p = p ^. $ addr . postcode
|
||||||
|
-- |from|get| Focus |
|
||||||
|
```
|
||||||
|
|
||||||
|
Es gibt drölf-zillionen weitere Infix-Operatoren (für Folds,
|
||||||
|
Listenkonvertierungen, -traversierungen, …)
|
||||||
|
|
||||||
|
## Virtuelle Felder
|
||||||
|
|
||||||
|
Man kann mit Lenses sogar Felder emulieren, die gar nicht da sind. Angenommen
|
||||||
|
folgender Code:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Temp = T { _fahrenheit :: Float }
|
||||||
|
|
||||||
|
$(makeLenses ''Temp)
|
||||||
|
-- liefert Lens: fahrenheit :: Lens Temp Float
|
||||||
|
|
||||||
|
centigrade :: Lens Temp Float
|
||||||
|
centigrade centi_fn (T faren)
|
||||||
|
= (\centi' -> T (cToF centi'))
|
||||||
|
<$> (centi_fn (fToC faren))
|
||||||
|
-- cToF & fToC as Converter-Functions defined someplace else
|
||||||
|
```
|
||||||
|
|
||||||
|
Hiermit kann man dann auch Funktionen, die auf Grad-Celsius rechnen auf Daten
|
||||||
|
anwenden, die eigenlich nur Fahrenheit speichern, aber eine Umrechnung
|
||||||
|
bereitstellen. Analog kann man auch einen Zeit-Datentypen definieren, der
|
||||||
|
intern mit Sekunden rechnet (und somit garantiert frei von Fehlern wie -3
|
||||||
|
Minuten oder 37 Stunden ist)
|
||||||
|
|
||||||
|
## Non-Record Strukturen
|
||||||
|
|
||||||
|
Das ganze kann man auch parametrisieren und auf Non-Record-Strukturen
|
||||||
|
anwenden.
|
||||||
|
Beispielhaft an einer Map verdeutlicht:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
-- from Data.Lens.At
|
||||||
|
at :: Ord k => k -> Lens' (Map k v) (Maybe v)
|
||||||
|
|
||||||
|
-- oder identisch, wenn man die Lens' auflöst:
|
||||||
|
at :: Ord k, forall f. Functor f => k -> (Maybe v -> f Maybe v) -> Map k v -> f Map k v
|
||||||
|
|
||||||
|
at k mb_fn m
|
||||||
|
= wrap <$> (mb_fn mv)
|
||||||
|
where
|
||||||
|
mv = Map.lookup k m
|
||||||
|
|
||||||
|
wrap :: Maybe v -> Map k v
|
||||||
|
wrap (Just v') = Map.insert k v' m
|
||||||
|
wrap Nothing = case mv of
|
||||||
|
Nothing -> m
|
||||||
|
Just _ -> Map.delete k m
|
||||||
|
|
||||||
|
-- mb_fn :: Maybe v -> f Maybe v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Weitere Beispiele
|
||||||
|
|
||||||
|
- Bitfields auf Strukturen die Bits haben (Ints, …) in Data.Bits.Lens
|
||||||
|
- Web-scraper in Package hexpat-lens
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
p ^.. _HTML' . to allNodes
|
||||||
|
. traverse . named "a"
|
||||||
|
. traverse . ix "href"
|
||||||
|
. filtered isLocal
|
||||||
|
. to trimSpaces
|
||||||
|
```
|
||||||
|
|
||||||
|
Zieht alle externen Links aus dem gegebenen HTML-Code in p um weitere ziele
|
||||||
|
fürs crawlen zu finden.
|
||||||
|
|
||||||
|
## Erweiterungen
|
||||||
|
|
||||||
|
Bisher hatten wir Lenses nur auf Funktoren F. Die nächstmächtigere Klasse ist
|
||||||
|
Applicative.
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
type Traversal' s a = forall f. Applicative f
|
||||||
|
=> (a -> f a) -> (s -> f s)
|
||||||
|
```
|
||||||
|
|
||||||
|
Da wir den Container identisch lassen (weder s noch a wurde angefasst) muss
|
||||||
|
sich
|
||||||
|
etwas anderes ändern. Statt eines einzelnen Focus erhalten wir viele Foci.
|
||||||
|
|
||||||
|
Was ist ein Applicative überhaupt? Eine schwächere Monade (nur 1x Anwendung
|
||||||
|
und
|
||||||
|
kein Bind - dafür kann man die beliebig oft hintereinanderhängen).
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
class Functor f => Applicative f where
|
||||||
|
pure :: a -> f a
|
||||||
|
(<*>) :: f (a -> b) -> f a -> f b
|
||||||
|
|
||||||
|
-- Monade als Applicative:
|
||||||
|
pure = return
|
||||||
|
mf <*> mx = do { f <- mf; x <- mx; return (f x) }
|
||||||
|
```
|
||||||
|
|
||||||
|
Recap: Was macht eine Lens:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
data Adress = A { _road :: String
|
||||||
|
, _city :: String
|
||||||
|
, _postcode :: String }
|
||||||
|
|
||||||
|
road :: Lens' Adress String
|
||||||
|
road elt_fn (A r c p) = (\r' -> A r' c p) <$> (elt_fn r)
|
||||||
|
-- | "Hole" | | Thing to put in|
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn man nun road & city gleichzeitig bearbeiten will:
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
addr_strs :: Traversal' Address String
|
||||||
|
addr_strs elt_fn (A r c p)
|
||||||
|
= ... (\r' c' -> A r' c' p) .. (elt_fn r) .. (elt_fn c) ..
|
||||||
|
-- | function with 2 "Holes"| first Thing | second Thing
|
||||||
|
```
|
||||||
|
|
||||||
|
fmap kann nur 1 Loch stopfen, aber nicht mit n Löchern umgehen. Applicative
|
||||||
|
mit
|
||||||
|
<\*> kann das.
|
||||||
|
Somit gibt sich
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
addr_strs :: Traversal' Address String
|
||||||
|
addr_strs elt_fn (A r c p)
|
||||||
|
= pure (\r' c' -> A r' c' p) <*> (elt_fn r) <*> (elt_fn c)
|
||||||
|
-- lift in Appl. | function with 2 "Holes"| first Thing | second Thing
|
||||||
|
-- oder kürzer
|
||||||
|
addr_strs :: Traversal' Address String
|
||||||
|
addr_strs elt_fn (A r c p)
|
||||||
|
= (\r' c' -> A r' c' p) <$> (elt_fn r) <*> (elt_fn c)
|
||||||
|
-- pure x <*> y == x <$> y
|
||||||
|
```
|
||||||
|
|
||||||
|
Wie würd eine modify-funktion aussehen?
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
over :: Lens' s a -> (a -> a) -> s -> s
|
||||||
|
over ln f = runIdentity . ln (Identity . f)
|
||||||
|
|
||||||
|
over :: Traversal' s a -> (a -> a) -> s -> s
|
||||||
|
over ln f = runIdentity . ln (Identity . f)
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Code ist derselbe - nur der Typ ist generischer. Auch die anderen Dinge
|
||||||
|
funktioniert diese Erweiterung (für Identity und Const muss man noch ein paar
|
||||||
|
dummy-Instanzen schreiben um sie von Functor auf Applicative oder Monad zu heben
|
||||||
|
|
||||||
|
- konkret reicht hier die Instanzierung von Monoid). In der Lens-Library ist
|
||||||
|
daher meist Monad m statt Functor f gefordert.
|
||||||
|
|
||||||
|
## Wozu dienen die Erweiterungen?
|
||||||
|
|
||||||
|
Man kann mit Foci sehr selektiv vorgehen. Auch kann man diese durch Funktionen
|
||||||
|
steuern. Beispisweise eine Function anwenden auf
|
||||||
|
|
||||||
|
- Jedes 2. Listenelement
|
||||||
|
- Alle graden Elemente in einem Baum
|
||||||
|
- Alle Namen in einer Tabelle, deren Gehalt > 10.000€ ist
|
||||||
|
|
||||||
|
Traversals und Lenses kann man trivial kombinieren (`lens . lens` => `lens`,
|
||||||
|
`lens . traversal` => `traversal` etc.)
|
||||||
|
|
||||||
|
## Wie es in Lens wirklich aussieht
|
||||||
|
|
||||||
|
In diesem Artikel wurde nur auf Monomorphic Lenses eingegangen. In der
|
||||||
|
richtigen
|
||||||
|
Library ist eine Lens
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
type Lens' s a = Lens s s a a
|
||||||
|
type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)
|
||||||
|
```
|
||||||
|
|
||||||
|
sodass sich auch die Typen ändern können um z.B. automatisch einen
|
||||||
|
Konvertierten
|
||||||
|
(sicheren) Typen aus einer unsicheren Datenstruktur zu geben.
|
||||||
|
|
||||||
|
Die modify-Funktion over ist auch
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
> over :: Profunctor p => Setting p s t a b -> p a b -> s -> t
|
||||||
|
```
|
||||||
|
|
||||||
|
> _Edward is deeply in thrall to abstractionitis_ - Simon Peyton Jones
|
||||||
|
|
||||||
|
Lens alleine definiert 39 newtypes, 34 data-types und 194 Typsynonyme…
|
||||||
|
Ausschnitt
|
||||||
|
|
||||||
|
```{ .haskell }
|
||||||
|
-- traverseOf :: Functor f => Iso s t a b -> (a -> f b) -> s -> f t
|
||||||
|
-- traverseOf :: Functor f => Lens s t a b -> (a -> f b) -> s -> f t
|
||||||
|
-- traverseOf :: Applicative f => Traversal s t a b -> (a -> f b) -> s -> f t
|
||||||
|
|
||||||
|
traverseOf :: Over p f s t a b -> p a (f b) -> s -> f t
|
||||||
|
```
|
||||||
|
|
||||||
|
dafuq?
|
197
Coding/Haskell/Webapp-Example/Main.hs.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
title: "Webapp-Example: Main.hs"
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
date: 2020-04-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Wie man das verwendet, siehe [Webapp-Example](index.qmd).
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
module MyService where
|
||||||
|
|
||||||
|
-- generische imports aus den dependencies/base, nicht in der prelude
|
||||||
|
import Codec.MIME.Type
|
||||||
|
import Configuration.Dotenv as Dotenv
|
||||||
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Catch
|
||||||
|
import Control.Monad.Except
|
||||||
|
import Conversion
|
||||||
|
import Conversion.Text ()
|
||||||
|
import Data.Binary.Builder
|
||||||
|
import Data.String (IsString (..))
|
||||||
|
import Data.Time
|
||||||
|
import Data.Time.Clock
|
||||||
|
import Data.Time.Format
|
||||||
|
import Data.Default
|
||||||
|
import Network.HostName
|
||||||
|
import Network.HTTP.Client as HTTP hiding
|
||||||
|
(withConnection)
|
||||||
|
import Network.HTTP.Types (Status, statusCode)
|
||||||
|
import Network.Mom.Stompl.Client.Queue
|
||||||
|
import Network.Wai (Middleware)
|
||||||
|
import Network.Wai.Logger
|
||||||
|
import Network.Wai.Middleware.Cors
|
||||||
|
import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
|
||||||
|
logStdout,
|
||||||
|
mkRequestLogger,
|
||||||
|
outputFormat)
|
||||||
|
import Servant.Client (mkClientEnv,
|
||||||
|
parseBaseUrl)
|
||||||
|
import System.Directory
|
||||||
|
import System.Envy
|
||||||
|
import System.IO
|
||||||
|
import System.Log.FastLogger
|
||||||
|
import Text.PrettyPrint.GenericPretty
|
||||||
|
|
||||||
|
-- generische imports, aber qualified, weil es sonst zu name-clashes kommt
|
||||||
|
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
-- import qualified Data.ByteString.Char8 as BS8
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings)
|
||||||
|
import qualified Network.Mom.Stompl.Client.Queue as AMQ
|
||||||
|
import qualified Network.Wai as WAI
|
||||||
|
|
||||||
|
-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries
|
||||||
|
import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert
|
||||||
|
import MyService.Types -- weitere Type (s. nächste box)
|
||||||
|
import MyServiceGen.API as MS -- aus der generierten library
|
||||||
|
|
||||||
|
|
||||||
|
myServicemain :: IO ()
|
||||||
|
myServicemain = do
|
||||||
|
-- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt
|
||||||
|
void $ loadFile $ Dotenv.Config [".env"] [] False
|
||||||
|
-- Config holen (defaults + overrides aus dem Environment)
|
||||||
|
sc@ServerConfig{..} <- decodeWithDefaults defConfig
|
||||||
|
-- Backend-Setup
|
||||||
|
-- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices
|
||||||
|
let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout }
|
||||||
|
createBackend url proxy = do
|
||||||
|
manager <- newManager . managerSetProxy proxy
|
||||||
|
$ defaultHTTPSSettings
|
||||||
|
url' <- parseBaseUrl url
|
||||||
|
return (mkClientEnv manager url')
|
||||||
|
internalProxy = case myserviceInternalProxyUrl of
|
||||||
|
"" -> noProxy
|
||||||
|
url -> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort
|
||||||
|
-- externalProxy = case myserviceExternalProxyUrl of
|
||||||
|
-- "" -> noProxy
|
||||||
|
-- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort
|
||||||
|
|
||||||
|
-- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.
|
||||||
|
calls <- (,)
|
||||||
|
<$> createBackend myserviceAUri internalProxy
|
||||||
|
<*> createBackend myserviceBUri internalProxy
|
||||||
|
|
||||||
|
-- Logging-Setup
|
||||||
|
hSetBuffering stdout LineBuffering
|
||||||
|
hSetBuffering stderr LineBuffering
|
||||||
|
|
||||||
|
|
||||||
|
-- Infos holen, brauchen wir später
|
||||||
|
myName <- getHostName
|
||||||
|
today <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime
|
||||||
|
|
||||||
|
|
||||||
|
-- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten
|
||||||
|
amqPost <- newTQueueIO
|
||||||
|
|
||||||
|
|
||||||
|
-- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben.
|
||||||
|
bracket
|
||||||
|
-- logfiles öffnen
|
||||||
|
(LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode
|
||||||
|
<*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode
|
||||||
|
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode
|
||||||
|
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode
|
||||||
|
)
|
||||||
|
-- und bei exception/beendigung schlißen.h
|
||||||
|
(\(LogFiles a b c d) -> mapM_ hClose [a,b,c,d])
|
||||||
|
$ \logfiles -> do
|
||||||
|
|
||||||
|
|
||||||
|
-- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.
|
||||||
|
let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m ()
|
||||||
|
iolog = printLogFilesIO logfiles :: [LogItem] -> IO ()
|
||||||
|
|
||||||
|
|
||||||
|
-- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei
|
||||||
|
-- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion
|
||||||
|
-- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp.
|
||||||
|
server = MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log
|
||||||
|
}
|
||||||
|
config = MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/"
|
||||||
|
iolog . pure . Info $ "Using Server configuration:"
|
||||||
|
iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;)
|
||||||
|
, myserviceMongoPassword = "******"
|
||||||
|
}
|
||||||
|
-- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ
|
||||||
|
void $ forkIO $ keepActiveMQConnected sc iolog amqPost
|
||||||
|
-- logging-Framework erzeugen
|
||||||
|
loggingMW <- loggingMiddleware
|
||||||
|
-- server starten
|
||||||
|
if myserviceDebug
|
||||||
|
then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server
|
||||||
|
else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server
|
||||||
|
|
||||||
|
|
||||||
|
-- Sollte bald in die Library hs-stomp ausgelagert werden
|
||||||
|
-- ist ein Beispiel für einen ActiveMQ-Dumper
|
||||||
|
keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO ()
|
||||||
|
keepActiveMQConnected sc@ServerConfig{..} printLog var = do
|
||||||
|
res <- handle (\(e :: SomeException) -> do
|
||||||
|
printLog . pure . Error $ "Exception in AMQ-Thread: "<>show e
|
||||||
|
return $ Right ()
|
||||||
|
) $ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up.
|
||||||
|
printLog . pure . Info $ "AMQ: connecting..."
|
||||||
|
withConnection myserviceActivemqHost myserviceActivemqPort [ OAuth myserviceActivemqUsername myserviceActivemqPassword
|
||||||
|
, OTmo (30*1000) {- 30 sec timeout -}
|
||||||
|
]
|
||||||
|
[] $ \c -> do
|
||||||
|
let oconv = return
|
||||||
|
printLog . pure . Info $ "AMQ: connected"
|
||||||
|
withWriter c "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do
|
||||||
|
printLog . pure . Info $ "AMQ: queue created"
|
||||||
|
let postfun = writeQ writer (Type (Application "json") []) []
|
||||||
|
void $ race
|
||||||
|
(forever $ atomically (readTQueue var) >>= postfun)
|
||||||
|
(threadDelay (600*1000*1000)) -- wait 10 Minutes
|
||||||
|
-- close writer
|
||||||
|
-- close connection
|
||||||
|
-- get outside of all try/handle/...-constructions befor recursing.
|
||||||
|
case res of
|
||||||
|
Left ex -> do
|
||||||
|
printLog . pure . Error $ "AMQ: "<>show ex
|
||||||
|
keepActiveMQConnected sc printLog var
|
||||||
|
Right _ -> keepActiveMQConnected sc printLog var
|
||||||
|
|
||||||
|
|
||||||
|
-- Beispiel für eine Custom-Logging-Middleware.
|
||||||
|
-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben.
|
||||||
|
-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;)
|
||||||
|
loggingMiddleware :: IO Middleware
|
||||||
|
loggingMiddleware = liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out }
|
||||||
|
where
|
||||||
|
out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr
|
||||||
|
out _ r status _ _ payload _
|
||||||
|
| statusCode status < 300 = ""
|
||||||
|
| statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n"
|
||||||
|
| otherwise = toLogStr (show r) <> "\n"
|
||||||
|
|
||||||
|
```
|
92
Coding/Haskell/Webapp-Example/MyService_Types.hs.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
title: "Webapp-Example: MyService/Types.hs"
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Code
|
||||||
|
date: 2020-04-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Anleitung siehe [Webapp-Example](index.qmd).
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveFunctor #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DerivingVia #-}
|
||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
module MyService.Types where
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON, ToJSON)
|
||||||
|
import Data.Text
|
||||||
|
import Data.Time.Clock
|
||||||
|
import GHC.Generics
|
||||||
|
import System.Envy
|
||||||
|
import Text.PrettyPrint (text)
|
||||||
|
import Text.PrettyPrint.GenericPretty
|
||||||
|
|
||||||
|
-- Out hat hierfür keine Instanzen, daher kurz eine einfach Definition.
|
||||||
|
instance Out Text where
|
||||||
|
doc = text . unpack
|
||||||
|
docPrec i a = text $ showsPrec i a ""
|
||||||
|
|
||||||
|
instance Out UTCTime where
|
||||||
|
doc = text . show
|
||||||
|
docPrec i a = text $ showsPrec i a ""
|
||||||
|
|
||||||
|
-- Der ServerConfig-Typ. Wird mit den defaults unten initialisiert, dann mit den Variablen aus der .env-Datei überschrieben und zum Schluss können Serveradmins diese via $MYSERVICE_FOO nochmal überschreiben.
|
||||||
|
data ServerConfig = ServerConfig
|
||||||
|
{ myserviceHost :: String -- ^ Environment: $MYSERVICE_HOST
|
||||||
|
, myservicePort :: Int -- ^ Environment: $MYSERVICE_PORT
|
||||||
|
, myserviceMaxTimeout :: Int -- ^ Environment: $MYSERVICE_MAX_TIMEOUT
|
||||||
|
, myserviceInternalProxyUrl :: String -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_URL
|
||||||
|
, myserviceInternalProxyPort :: Int -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_PORT
|
||||||
|
, myserviceExternalProxyUrl :: String -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_URL
|
||||||
|
, myserviceExternalProxyPort :: Int -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_PORT
|
||||||
|
, myserviceActivemqHost :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_HOST
|
||||||
|
, myserviceActivemqPort :: Int -- ^ Environment: $MYSERVICE_ACTIVEMQ_PORT
|
||||||
|
, myserviceActivemqUsername :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_USERNAME
|
||||||
|
, myserviceActivemqPassword :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_PASSWORD
|
||||||
|
, myserviceMongoUsername :: String -- ^ Environment: $MYSERVICE_MONGO_USERNAME
|
||||||
|
, myserviceMongoPassword :: String -- ^ Environment: $MYSERVICE_MONGO_PASSWORD
|
||||||
|
, myserviceDebug :: Bool -- ^ Environment: $MYSERVICE_DEBUG
|
||||||
|
} deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
|
-- Default-Konfigurations-Instanz für diesen Service.
|
||||||
|
instance DefConfig ServerConfig where
|
||||||
|
defConfig = ServerConfig "0.0.0.0" 8080 20
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
False
|
||||||
|
|
||||||
|
-- Kann auch aus dem ENV gefüllt werden
|
||||||
|
instance FromEnv ServerConfig
|
||||||
|
-- Und hübsch ausgegeben werden.
|
||||||
|
instance Out ServerConfig
|
||||||
|
|
||||||
|
|
||||||
|
instance Out Response
|
||||||
|
instance FromBSON Repsonse -- FromBSON-Instanz geht immer davon aus, dass alle keys da sind (ggf. mit null bei Nothing).
|
||||||
|
```
|
598
Coding/Haskell/Webapp-Example/index.qmd
Normal file
@ -0,0 +1,598 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Haskell
|
||||||
|
- Tutorial
|
||||||
|
categories:
|
||||||
|
- Haskell
|
||||||
|
- Tutorial
|
||||||
|
title: Webapp-Development in Haskell
|
||||||
|
abstract: |
|
||||||
|
Step-by-Step-Anleitung, wie man ein neues Projekt mit einer bereits erprobten Pipeline erstellt.
|
||||||
|
execute:
|
||||||
|
eval: false
|
||||||
|
date: 2020-04-01
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition der API
|
||||||
|
|
||||||
|
Erster Schritt ist immer ein wünsch-dir-was bei der Api-Defenition.
|
||||||
|
|
||||||
|
Die meisten Services haben offensichtliche Anforderungen (Schnittstellen nach
|
||||||
|
draußen, Schnittstellen intern, ...). Diese kann man immer sehr gut in einem
|
||||||
|
`Request -> Response`-Model erfassen.
|
||||||
|
|
||||||
|
Um die Anforderungen und Möglichkeiten des jeweiligen Services sauber zu
|
||||||
|
erfassen und automatisiert zu prüfen, dummy-implementationen zu bekommen und
|
||||||
|
vieles andere mehr, empfiehlt es sich den
|
||||||
|
**Openapi-generator** zu nutzen.
|
||||||
|
|
||||||
|
Diese Definition läuft über openapi-v3 und kann z.b. mit Echtzeit-Vorschau im
|
||||||
|
<http://editor.swagger.io/> erspielen. Per Default ist der noch auf openapi-v2
|
||||||
|
(aka swagger), kann aber auch v3.
|
||||||
|
|
||||||
|
Nach der Definition, was man am Ende haben möchte, muss man sich entscheiden, in
|
||||||
|
welcher Sprache man weiter entwickelt. Ich empfehle aus verschiedenen Gründen
|
||||||
|
primär 2 Sprachen: Python-Microservices (weil die ML-Libraries sehr gut sind,
|
||||||
|
allerdings Änderungen meist schwer sind und der Code wenig robust - meist nur 1
|
||||||
|
API-Endpunkt pro service) und Haskell (Stabilität, Performace, leicht zu ändern,
|
||||||
|
gut anzupassen).
|
||||||
|
|
||||||
|
Im folgenden wird (aus offensichtlichen Gründen) nur auf das Haskell-Projekt
|
||||||
|
eingegangen.
|
||||||
|
|
||||||
|
## Startprojekt in Haskell
|
||||||
|
|
||||||
|
### Erstellen eines neuen Projektes
|
||||||
|
|
||||||
|
Zunächst erstellen wir in normales Haskell-Projekt ohne Funktionalität &
|
||||||
|
Firlefanz:
|
||||||
|
|
||||||
|
```{.bash}
|
||||||
|
stack new myservice
|
||||||
|
```
|
||||||
|
|
||||||
|
Dies erstellt ein neues Verzeichnis und das generelle scaffolding. Nach einer
|
||||||
|
kurzen Anpassung der `stack.yaml` (resolver auf unserer setzen; aktuell:
|
||||||
|
`lts-17.4`) fügen wir am Ende der Datei
|
||||||
|
|
||||||
|
```{.yaml}
|
||||||
|
allow-newer: true
|
||||||
|
ghc-options:
|
||||||
|
"$locals": -fwrite-ide-info
|
||||||
|
```
|
||||||
|
|
||||||
|
ein. Anschließend organisieren™ wir uns noch eine gute `.gitignore` und
|
||||||
|
initialisieren das git mittels
|
||||||
|
`git init; git add .; git commit -m "initial scaffold"`
|
||||||
|
|
||||||
|
### Generierung der API
|
||||||
|
|
||||||
|
Da die API immer wieder neu generiert werden kann (und sollte!) liegt sich in
|
||||||
|
einem unterverzeichnis des Hauptprojektes.
|
||||||
|
|
||||||
|
Initial ist es das einfachste ein leeres temporäres Verzeichnis woanders zu
|
||||||
|
erstellen, die `api-doc.yml` hinein kopieren und folgendes ausführen:
|
||||||
|
|
||||||
|
```{.bash}
|
||||||
|
openapi-generator generate -g haskell -o . -i api-doc.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses erstellt einem dann eine komplette library inkl. Datentypen. Wichtig: Der
|
||||||
|
Name in der `api-doc` sollte vom Namen des Services (oben `myservice`)
|
||||||
|
abweichen - entweder in Casing oder im Namen direkt. Suffixe wie API schneidet
|
||||||
|
der Generator hier leider ab. (Wieso das ganze? Es entstehen nachher 2
|
||||||
|
libraries, `foo` & `fooAPI`. Da der generator das API abschneidet endet man mit
|
||||||
|
foo & foo und der compiler meckert, dass er nicht weiß, welche lib gemeint ist).
|
||||||
|
|
||||||
|
danach: wie gewohnt `git init; git add .; git commit -m "initial"`. Auf dem
|
||||||
|
Server der Wahl (github, gitea, gitlab, ...) nun ein Repository erstellen (am
|
||||||
|
Besten: `myserviceAPI` - nach Konvention ist alles auf API endend
|
||||||
|
autogeneriert!) und den Anweisungen nach ein remote hinzufügen & pushen.
|
||||||
|
|
||||||
|
#### Wieder zurück im Haskell-Service
|
||||||
|
|
||||||
|
In unserem eigentlichen Service müssen wir nun die API einbinden. Dazu erstellen
|
||||||
|
wir ein Verzeichnis `libs` (Konvention) und machen ein
|
||||||
|
`git submodule add <repository-url> libs/myserviceAPI`
|
||||||
|
|
||||||
|
Git hat nun die API in das submodul gepackt und wir können das oben erstellte
|
||||||
|
temporäre Verzeichnis wieder löschen.
|
||||||
|
|
||||||
|
Anschließend müssen wir stack noch erklären, dass wir die API da nun liegen
|
||||||
|
haben und passen wieder die `stack.yaml` an, indem wir das Verzeichnis unter
|
||||||
|
packages hinzufügen.
|
||||||
|
|
||||||
|
```{.yaml}
|
||||||
|
packages:
|
||||||
|
- .
|
||||||
|
- libs/myserviceAPI # <<
|
||||||
|
```
|
||||||
|
|
||||||
|
Nun können wir in der `package.yaml` (oder `myservice.cabal`, falls kein `hpack`
|
||||||
|
verwendet wird) unter den dependencies unsere API hinzufügen (name wie die
|
||||||
|
cabal-Datei in `libs/myserviceAPI`).
|
||||||
|
|
||||||
|
### Einbinden anderer Microservices
|
||||||
|
|
||||||
|
Funktioniert komplett analog zu dem vorgehen oben (ohne das generieren natürlich
|
||||||
|
:grin:). `stack.yaml` editieren und zu den packages hinzufügen:
|
||||||
|
|
||||||
|
```{.yaml}
|
||||||
|
packages:
|
||||||
|
- .
|
||||||
|
- libs/myserviceAPI
|
||||||
|
- libs/myCoolMLServiceAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
in der `package.yaml` (oder der cabal) die dependencies hinzufügen und schon
|
||||||
|
haben wir die Features zur Verfügung und können gegen diese Services reden.
|
||||||
|
|
||||||
|
### Entfernen von anderen Technologien/Microservices
|
||||||
|
|
||||||
|
In git ist das entfernen von Submodules etwas frickelig, daher hier ein
|
||||||
|
copy&paste der
|
||||||
|
[GitHub-Antwort](https://gist.github.com/myusuf3/7f645819ded92bda6677):
|
||||||
|
|
||||||
|
```{.bash}
|
||||||
|
## Remove the submodule entry from .git/config
|
||||||
|
git submodule deinit -f path/to/submodule
|
||||||
|
|
||||||
|
## Remove the submodule directory from the superproject's .git/modules directory
|
||||||
|
rm-rf .git/modules/path/to/submodule
|
||||||
|
|
||||||
|
## Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule
|
||||||
|
git rm-f path/to/submodule
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls das nicht klappt, gibt es alternative Vorschläge unter dem Link oben.
|
||||||
|
|
||||||
|
### Woher weiss ich, was wo liegt? Dokumentation? Halloo??
|
||||||
|
|
||||||
|
Keine Panik. Ein `stack haddock --open` hilft da. Das generiert die
|
||||||
|
Dokumentation für alle in der `package.yaml` (oder cabal-file) eingetragenen
|
||||||
|
dependencies inkl. aller upstream-dependencies. Man bekommt also eine komplette
|
||||||
|
lokale Dokumentation von allem. Geöffnet wird dann die Paket-Startseite inkl.
|
||||||
|
der direkten dependencies:
|
||||||
|
|
||||||
|
Es gibt 2 wichtige Pfade im Browser:
|
||||||
|
|
||||||
|
- `...../all/index.html` - hier sind alle Pakete aufgeführt
|
||||||
|
- `...../index.html` - hier sind nur die direkten dependencies aufgeführt.
|
||||||
|
|
||||||
|
Wenn man einen lokalen Webserver startet kann man mittels "s" auch die
|
||||||
|
interaktive Suche öffnen (Suche nach Typen, Funktionen, Signaturen, etc.). In
|
||||||
|
Bash mit `python3` geht das z.b. einfach über:
|
||||||
|
|
||||||
|
```{.bash}
|
||||||
|
cd $(stack path --local-doc-root)
|
||||||
|
python3 -m SimpleHTTPServer 8000
|
||||||
|
firefox "http://localhost:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation des Services und Start
|
||||||
|
|
||||||
|
#### Loader/Bootstrapper
|
||||||
|
|
||||||
|
Generelles Vorgehen:
|
||||||
|
|
||||||
|
- in `app/Main.hs`: Hier ist quasi immer nur eine Zeile drin:
|
||||||
|
`main = myServiceMain`
|
||||||
|
|
||||||
|
Grund: Applications tauchen nicht im Haddock auf. Also haben wir ein
|
||||||
|
"src"-Modul, welches hier nur geladen & ausgeführt wird.
|
||||||
|
|
||||||
|
- in `src/MyService.hs`: `myServiceMain :: IO ()` definieren
|
||||||
|
|
||||||
|
Für die Main kann man prinzipiell eine Main andere Services copy/pasten. Im
|
||||||
|
folgenden eine Annotierte main-Funktion - zu den einzelnen Voraussetzungen
|
||||||
|
kommen wir im Anschluss.
|
||||||
|
|
||||||
|
{{< dstart summary="Main.hs anzeigen" >}}
|
||||||
|
|
||||||
|
```{.haskell code-fold=true code-summary="Code anzeigen"}
|
||||||
|
{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
module MyService where
|
||||||
|
|
||||||
|
-- generische imports aus den dependencies/base, nicht in der prelude
|
||||||
|
import Codec.MIME.Type
|
||||||
|
import Configuration.Dotenv as Dotenv
|
||||||
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Catch
|
||||||
|
import Control.Monad.Except
|
||||||
|
import Conversion
|
||||||
|
import Conversion.Text ()
|
||||||
|
import Data.Binary.Builder
|
||||||
|
import Data.String (IsString (..))
|
||||||
|
import Data.Time
|
||||||
|
import Data.Time.Clock
|
||||||
|
import Data.Time.Format
|
||||||
|
import Data.Default
|
||||||
|
import Network.HostName
|
||||||
|
import Network.HTTP.Client as HTTP hiding
|
||||||
|
(withConnection)
|
||||||
|
import Network.HTTP.Types (Status, statusCode)
|
||||||
|
import Network.Mom.Stompl.Client.Queue
|
||||||
|
import Network.Wai (Middleware)
|
||||||
|
import Network.Wai.Logger
|
||||||
|
import Network.Wai.Middleware.Cors
|
||||||
|
import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
|
||||||
|
logStdout,
|
||||||
|
mkRequestLogger,
|
||||||
|
outputFormat)
|
||||||
|
import Servant.Client (mkClientEnv,
|
||||||
|
parseBaseUrl)
|
||||||
|
import System.Directory
|
||||||
|
import System.Envy
|
||||||
|
import System.IO
|
||||||
|
import System.Log.FastLogger
|
||||||
|
import Text.PrettyPrint.GenericPretty
|
||||||
|
|
||||||
|
-- generische imports, aber qualified, weil es sonst zu name-clashes kommt
|
||||||
|
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
-- import qualified Data.ByteString.Char8 as BS8
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings)
|
||||||
|
import qualified Network.Mom.Stompl.Client.Queue as AMQ
|
||||||
|
import qualified Network.Wai as WAI
|
||||||
|
|
||||||
|
-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries
|
||||||
|
import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert
|
||||||
|
import MyService.Types -- weitere Type (s. nächste box)
|
||||||
|
import MyServiceGen.API as MS -- aus der generierten library
|
||||||
|
|
||||||
|
|
||||||
|
myServicemain :: IO ()
|
||||||
|
myServicemain = do
|
||||||
|
-- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt
|
||||||
|
void $ loadFile $ Dotenv.Config [".env"] [] False
|
||||||
|
-- Config holen (defaults + overrides aus dem Environment)
|
||||||
|
sc@ServerConfig{..} <- decodeWithDefaults defConfig
|
||||||
|
-- Backend-Setup
|
||||||
|
-- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices
|
||||||
|
let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout }
|
||||||
|
createBackend url proxy = do
|
||||||
|
manager <- newManager . managerSetProxy proxy
|
||||||
|
$ defaultHTTPSSettings
|
||||||
|
url' <- parseBaseUrl url
|
||||||
|
return (mkClientEnv manager url')
|
||||||
|
internalProxy = case myserviceInternalProxyUrl of
|
||||||
|
"" -> noProxy
|
||||||
|
url -> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort
|
||||||
|
-- externalProxy = case myserviceExternalProxyUrl of
|
||||||
|
-- "" -> noProxy
|
||||||
|
-- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort
|
||||||
|
|
||||||
|
-- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.
|
||||||
|
calls <- (,)
|
||||||
|
<$> createBackend myserviceAUri internalProxy
|
||||||
|
<*> createBackend myserviceBUri internalProxy
|
||||||
|
|
||||||
|
-- Logging-Setup
|
||||||
|
hSetBuffering stdout LineBuffering
|
||||||
|
hSetBuffering stderr LineBuffering
|
||||||
|
|
||||||
|
|
||||||
|
-- Infos holen, brauchen wir später
|
||||||
|
myName <- getHostName
|
||||||
|
today <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime
|
||||||
|
|
||||||
|
|
||||||
|
-- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten
|
||||||
|
amqPost <- newTQueueIO
|
||||||
|
|
||||||
|
|
||||||
|
-- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben.
|
||||||
|
bracket
|
||||||
|
-- logfiles öffnen
|
||||||
|
(LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode
|
||||||
|
<*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode
|
||||||
|
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode
|
||||||
|
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode
|
||||||
|
)
|
||||||
|
-- und bei exception/beendigung schlißen.h
|
||||||
|
(\(LogFiles a b c d) -> mapM_ hClose [a,b,c,d])
|
||||||
|
$ \logfiles -> do
|
||||||
|
|
||||||
|
|
||||||
|
-- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.
|
||||||
|
let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m ()
|
||||||
|
iolog = printLogFilesIO logfiles :: [LogItem] -> IO ()
|
||||||
|
|
||||||
|
|
||||||
|
-- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei
|
||||||
|
-- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion
|
||||||
|
-- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp.
|
||||||
|
server = MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log
|
||||||
|
}
|
||||||
|
config = MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/"
|
||||||
|
iolog . pure . Info $ "Using Server configuration:"
|
||||||
|
iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;)
|
||||||
|
, myserviceMongoPassword = "******"
|
||||||
|
}
|
||||||
|
-- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ
|
||||||
|
void $ forkIO $ keepActiveMQConnected sc iolog amqPost
|
||||||
|
-- logging-Framework erzeugen
|
||||||
|
loggingMW <- loggingMiddleware
|
||||||
|
-- server starten
|
||||||
|
if myserviceDebug
|
||||||
|
then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server
|
||||||
|
else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server
|
||||||
|
|
||||||
|
|
||||||
|
-- Sollte bald in die Library hs-stomp ausgelagert werden
|
||||||
|
-- ist ein Beispiel für einen ActiveMQ-Dumper
|
||||||
|
keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO ()
|
||||||
|
keepActiveMQConnected sc@ServerConfig{..} printLog var = do
|
||||||
|
res <- handle (\(e :: SomeException) -> do
|
||||||
|
printLog . pure . Error $ "Exception in AMQ-Thread: "<>show e
|
||||||
|
return $ Right ()
|
||||||
|
) $ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up.
|
||||||
|
printLog . pure . Info $ "AMQ: connecting..."
|
||||||
|
withConnection myserviceActivemqHost myserviceActivemqPort [ OAuth myserviceActivemqUsername myserviceActivemqPassword
|
||||||
|
, OTmo (30*1000) {- 30 sec timeout -}
|
||||||
|
]
|
||||||
|
[] $ \c -> do
|
||||||
|
let oconv = return
|
||||||
|
printLog . pure . Info $ "AMQ: connected"
|
||||||
|
withWriter c "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do
|
||||||
|
printLog . pure . Info $ "AMQ: queue created"
|
||||||
|
let postfun = writeQ writer (Type (Application "json") []) []
|
||||||
|
void $ race
|
||||||
|
(forever $ atomically (readTQueue var) >>= postfun)
|
||||||
|
(threadDelay (600*1000*1000)) -- wait 10 Minutes
|
||||||
|
-- close writer
|
||||||
|
-- close connection
|
||||||
|
-- get outside of all try/handle/...-constructions befor recursing.
|
||||||
|
case res of
|
||||||
|
Left ex -> do
|
||||||
|
printLog . pure . Error $ "AMQ: "<>show ex
|
||||||
|
keepActiveMQConnected sc printLog var
|
||||||
|
Right _ -> keepActiveMQConnected sc printLog var
|
||||||
|
|
||||||
|
|
||||||
|
-- Beispiel für eine Custom-Logging-Middleware.
|
||||||
|
-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben.
|
||||||
|
-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;)
|
||||||
|
loggingMiddleware :: IO Middleware
|
||||||
|
loggingMiddleware = liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out }
|
||||||
|
where
|
||||||
|
out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr
|
||||||
|
out _ r status _ _ payload _
|
||||||
|
| statusCode status < 300 = ""
|
||||||
|
| statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n"
|
||||||
|
| otherwise = toLogStr (show r) <> "\n"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< dend >}}
|
||||||
|
|
||||||
|
#### Weitere Instanzen und Definitionen, die der Generator (noch) nicht macht
|
||||||
|
|
||||||
|
In der `Myservice.Types` werden ein paar hilfreiche Typen und Typ-Instanzen
|
||||||
|
definiert. Im Folgenden geht es dabei um Dinge für:
|
||||||
|
|
||||||
|
- `Envy`
|
||||||
|
- Laden von `$ENV_VAR` in Datentypen
|
||||||
|
- Definitionen für Default-Settings
|
||||||
|
- `ServerConfig`
|
||||||
|
- Definition der Server-Konfiguration & Benennung der Environment-Variablen
|
||||||
|
- `ExtraTypes`
|
||||||
|
- ggf. Paketweite extra-Typen, die der Generator nicht macht, weil sie nicht
|
||||||
|
aus der API kommen (z.B. cache)
|
||||||
|
- `Out`/`BSON`-Instanzen
|
||||||
|
- Der API-Generator generiert nur wenige Instanzen automatisch (z.B. `aeson`),
|
||||||
|
daher werden hier die fehlenden definiert.
|
||||||
|
- `BSON`: Kommunikation mit `MongoDB`
|
||||||
|
- `Out`: pretty-printing im Log
|
||||||
|
- Nur nötig, wenn man pretty-printing via `Out` statt über Generics wie z.b.
|
||||||
|
`pretty-generic` oder die automatische Show-Instanz via `prerryShow`
|
||||||
|
macht.
|
||||||
|
|
||||||
|
{{< dstart summary="Types.hs anzeigen" >}}
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||||
|
{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveFunctor #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DerivingVia #-}
|
||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
module MyService.Types where
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON, ToJSON)
|
||||||
|
import Data.Text
|
||||||
|
import Data.Time.Clock
|
||||||
|
import GHC.Generics
|
||||||
|
import System.Envy
|
||||||
|
import Text.PrettyPrint (text)
|
||||||
|
import Text.PrettyPrint.GenericPretty
|
||||||
|
|
||||||
|
-- Out hat hierfür keine Instanzen, daher kurz eine einfach Definition.
|
||||||
|
instance Out Text where
|
||||||
|
doc = text . unpack
|
||||||
|
docPrec i a = text $ showsPrec i a ""
|
||||||
|
|
||||||
|
instance Out UTCTime where
|
||||||
|
doc = text . show
|
||||||
|
docPrec i a = text $ showsPrec i a ""
|
||||||
|
|
||||||
|
-- Der ServerConfig-Typ. Wird mit den defaults unten initialisiert, dann mit den Variablen aus der .env-Datei überschrieben und zum Schluss können Serveradmins diese via $MYSERVICE_FOO nochmal überschreiben.
|
||||||
|
data ServerConfig = ServerConfig
|
||||||
|
{ myserviceHost :: String -- ^ Environment: $MYSERVICE_HOST
|
||||||
|
, myservicePort :: Int -- ^ Environment: $MYSERVICE_PORT
|
||||||
|
, myserviceMaxTimeout :: Int -- ^ Environment: $MYSERVICE_MAX_TIMEOUT
|
||||||
|
, myserviceInternalProxyUrl :: String -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_URL
|
||||||
|
, myserviceInternalProxyPort :: Int -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_PORT
|
||||||
|
, myserviceExternalProxyUrl :: String -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_URL
|
||||||
|
, myserviceExternalProxyPort :: Int -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_PORT
|
||||||
|
, myserviceActivemqHost :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_HOST
|
||||||
|
, myserviceActivemqPort :: Int -- ^ Environment: $MYSERVICE_ACTIVEMQ_PORT
|
||||||
|
, myserviceActivemqUsername :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_USERNAME
|
||||||
|
, myserviceActivemqPassword :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_PASSWORD
|
||||||
|
, myserviceMongoUsername :: String -- ^ Environment: $MYSERVICE_MONGO_USERNAME
|
||||||
|
, myserviceMongoPassword :: String -- ^ Environment: $MYSERVICE_MONGO_PASSWORD
|
||||||
|
, myserviceDebug :: Bool -- ^ Environment: $MYSERVICE_DEBUG
|
||||||
|
} deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
|
-- Default-Konfigurations-Instanz für diesen Service.
|
||||||
|
instance DefConfig ServerConfig where
|
||||||
|
defConfig = ServerConfig "0.0.0.0" 8080 20
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
0
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
False
|
||||||
|
|
||||||
|
-- Kann auch aus dem ENV gefüllt werden
|
||||||
|
instance FromEnv ServerConfig
|
||||||
|
-- Und hübsch ausgegeben werden.
|
||||||
|
instance Out ServerConfig
|
||||||
|
|
||||||
|
|
||||||
|
instance Out Response
|
||||||
|
instance FromBSON Repsonse -- FromBSON-Instanz geht immer davon aus, dass alle keys da sind (ggf. mit null bei Nothing).
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< dend >}}
|
||||||
|
|
||||||
|
#### Was noch zu tun ist
|
||||||
|
|
||||||
|
Den Service implementieren. Einfach ein neues Modul aufmachen (z.B.
|
||||||
|
`MyService.Handler` oder
|
||||||
|
`MyService.DieserEndpunktbereich`/`MyService.JenerEndpunktbereich`) und dort die
|
||||||
|
Funktion implementieren, die man in der `Main.hs` benutzt hat. In dem Handler
|
||||||
|
habt ihr dann keinen Stress mehr mit Validierung, networking, logging, etc. pp.
|
||||||
|
weil alles in der Main abgehandelt wurde und ihr nur noch den "Happy-Case"
|
||||||
|
implementieren müsst. Beispiel für unseren Handler oben:
|
||||||
|
|
||||||
|
```{.haskell}
|
||||||
|
myApiEndpointV1Post :: MonadIO m => ServerConfig -> (ClientEnv,ClientEnv) -> TQueue BS.ByteString -> ([LogItem] -> IO ()) -> Request -> m Response
|
||||||
|
myApiEndpointV1Post sc calls amqPost log req = do
|
||||||
|
liftIO . log $ [Info $ "recieved "<>pretty req] -- input-logging
|
||||||
|
liftIO . atomically . writeTQueue . LBS.toStrict $ "{\"hey Kibana, i recieved:\"" <> A.encode (pretty req) <> "}" -- log in activeMQ/Kibana
|
||||||
|
|
||||||
|
|
||||||
|
--- .... gaaaanz viel komplizierter code um die Response zu erhalten ;)
|
||||||
|
let ret = Response 1337 Nothing -- dummy-response ;)
|
||||||
|
-- gegeben wir haben eine gültige mongodb-pipe;
|
||||||
|
-- mehr logik will ich in die Beispiele nicht packen.
|
||||||
|
-- Man kann die z.b. als weiteren Wert in einer TMVar (damit man sie ändern & updaten kann) an die Funktion übergeben.
|
||||||
|
liftIO . access pipe master "DatabaseName" $ do
|
||||||
|
ifM (auth (myServiceMongoUsername sc) (myServiceMongoPassword sc)) (return ()) (liftIO . printLog . pure . Error $ "MongoDB: Login failed.")
|
||||||
|
save "DatabaseCollection" ["_id" =: 1337, "entry" =: ret] -- selbe id wie oben ;)
|
||||||
|
return ret
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese dummy-Antwort führt auf, wie gut man die ganzen Sachen mischen kann.
|
||||||
|
|
||||||
|
- Logging in die Dateien/`stdout` - je nach Konfiguration
|
||||||
|
- Logging von Statistiken in Kibana
|
||||||
|
- Speichern der Antwort in der MongoDB
|
||||||
|
- Generieren einer Serverantwort und ausliefern dieser über die Schnittstelle
|
||||||
|
|
||||||
|
#### Tipps & Tricks
|
||||||
|
|
||||||
|
##### Dateien, die statisch ausgeliefert werden sollen
|
||||||
|
|
||||||
|
Hierzu erstellt man ein Verzeichnis `static/` (Konvention; ist im generator so
|
||||||
|
generiert, dass das ausgeliefert wird). Packt man hier z.b. eine `index.html`
|
||||||
|
rein, erscheint die, wenn man den Service ansurft.
|
||||||
|
|
||||||
|
##### Wie bekomme ich diese fancy Preview hin?
|
||||||
|
|
||||||
|
Der Editor, der ganz am Anfang zum Einsatz gekommen ist, braucht nur die
|
||||||
|
`api-doc.yml` um diese Ansicht zu erzeugen. Daher empfiehlt sich hier ein
|
||||||
|
angepasster Fork davon indem die Pfade in der index.html korrigiert sind. Am
|
||||||
|
einfachsten (und von den meisten services so benutzt): In meiner Implementation
|
||||||
|
liegt dann nach dem starten auf <http://localhost:PORT/ui/> und kann direkt dort
|
||||||
|
getestet werden.
|
||||||
|
|
||||||
|
##### Wie sorge ich für bessere Warnungen, damit der Compiler meine Bugs fängt?
|
||||||
|
|
||||||
|
```{.bash}
|
||||||
|
stack build --file-watch --ghc-options '-freverse-errors -W -Wall -Wcompat' --interleaved-output
|
||||||
|
```
|
||||||
|
|
||||||
|
Was tut das?
|
||||||
|
|
||||||
|
- `--file-watch`: automatisches (minimales) kompilieren bei dateiänderungen
|
||||||
|
- `--ghc-options`
|
||||||
|
- `-freverse-errors`: Fehlermeldungen in umgekehrter Reihenfolge (Erster
|
||||||
|
Fehler ganz unten; wenig scrollen )
|
||||||
|
- `-W`: Warnungen an
|
||||||
|
- `-Wall`: Alle sinnvollen Warnungen an (im gegensatz zu `-Weverything`, was
|
||||||
|
WIRKLICH alles ist )
|
||||||
|
- `-Wcompat`: Warnungen für Sachen, die in der nächsten Compilerversion kaputt
|
||||||
|
brechen werden & vermieden werden sollten
|
||||||
|
- `--interleaved-output`: stack-log direkt ausgeben & nicht in Dateien schreiben
|
||||||
|
und die dann am ende zusammen cat\'en.
|
||||||
|
|
||||||
|
Um pro Datei Warnungen auszuschalten (z.B. weil man ganz sicher weiss, was man
|
||||||
|
tut -.-): `{-# OPTIONS_GHC -Wno-whatsoever #-}` als pragma in die Datei.
|
||||||
|
|
||||||
|
**Idealerweise sollte das Projekt keine Warnungen erzeugen.**
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Als Beispiel sei hier ein einfaches Docker-Build mit Jenkins-CI gezeigt, weil
|
||||||
|
ich das aus Gründen rumliegen hatte. Kann man analog in fast alle anderen CI
|
||||||
|
übersetzen.
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
|
||||||
|
Die angehängten Scripte gehen von einer Standard-Einrichtung aus (statische
|
||||||
|
Sachen in static, 2-3 händische Anpassungen auf das eigene Projekt nach
|
||||||
|
auspacken). Nachher liegt dann auch unter static/version die gebaute
|
||||||
|
Versionsnummer & kann abgerufen werden. In der `Dockerfile.release` und der
|
||||||
|
`Jenkinsfile` müssen noch Anpassungen gemacht werden. Konkret:
|
||||||
|
|
||||||
|
- in der `Dockerfile.release`: alle `<<<HIER>>>`-Stellen sinnvoll befüllen
|
||||||
|
- in der `Jenkinsfile` die defs für "servicename" und "servicebinary" ausfüllen.
|
||||||
|
Binary ist das, was bei stack exec aufgerufen wird; name ist der Image-Name
|
||||||
|
für das docker-repository.
|
||||||
|
|
||||||
|
#### Jenkins
|
||||||
|
|
||||||
|
Änderungen die dann noch gemacht werden müssen:
|
||||||
|
|
||||||
|
- git-repository URL anpassen
|
||||||
|
- Environment-Vars anpassen (\$BRANCH = test & live haben keine zusatzdinger im
|
||||||
|
docker-image-repository; ansonsten hat das image \$BRANCH im Namen)
|
||||||
|
|
||||||
|
Wenn das fertig gebaut ist, liegt im test/live-repository ein docker-image
|
||||||
|
namens `servicename:version`.
|
||||||
|
|
||||||
|
### OMG! Ich muss meine API ändern. Was mache ich nun?
|
||||||
|
|
||||||
|
1. api-doc.yml bearbeiten, wie gewünscht
|
||||||
|
2. mittels generator die Api & submodule neu generieren
|
||||||
|
3. ggf. custom Änderungen übernehmen (:Gitdiffsplit hilft)
|
||||||
|
4. Alle Compilerfehler + Warnungen in der eigentlichen Applikation fixen
|
||||||
|
5. If it comipilez, ship it! (Besser nicht :grin:)
|
76
Health/Issues.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: Mental Health
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
status: Incomplete
|
||||||
|
categories:
|
||||||
|
- Mental Health
|
||||||
|
- Article
|
||||||
|
- Struggles
|
||||||
|
date: 2019-01-01
|
||||||
|
date-modified: 2025-05-09
|
||||||
|
---
|
||||||
|
|
||||||
|
In modern times many people struggle with mental health issues - and a am by no
|
||||||
|
means an exception. The main issue is, that most people just don't talk about it
|
||||||
|
and suffer alone, thinking they are alone, and everyone else is just doing fine
|
||||||
|
in this hellscape of a modern society. At least that is what you see on several
|
||||||
|
social media platforms like Instagram etc.
|
||||||
|
|
||||||
|
So even despite my exceptional^[citation needed] successes that can be seen in
|
||||||
|
my [work](../About/Work.md) i always struggled with issues even to the point of
|
||||||
|
total breakdown. Of course i am also guilty of painting a rosy picture of me -
|
||||||
|
just look at [a summary of my experiences](../About/Experience.md) or the
|
||||||
|
awesome [things i did at university](../About/Extracurricular.md). If you only
|
||||||
|
read that it is hard to believe that i basically had to delay my studies from
|
||||||
|
2007 to 2010 because i wasn't even really able to leave the house.
|
||||||
|
|
||||||
|
Only thanks to the not-that-awful system in Germany and massive financial help
|
||||||
|
from my parents i was even able to pursue this way.
|
||||||
|
|
||||||
|
## What are my issues?
|
||||||
|
|
||||||
|
Well.. after 15 long years of therapy i finally get a hang of all my issues.
|
||||||
|
Many of those are related, some are just the result of coping mechanisms of
|
||||||
|
ignoring other issues.
|
||||||
|
|
||||||
|
Currently i am successfully diagnosed with
|
||||||
|
|
||||||
|
- social anxiety
|
||||||
|
- ADHD
|
||||||
|
- transgenderism
|
||||||
|
|
||||||
|
and i got a big suspician of
|
||||||
|
|
||||||
|
- autism
|
||||||
|
|
||||||
|
All in all: when i feel well, am good rested and have nothing critical coming up
|
||||||
|
i am more of what i would call a "high functioning Autist, but not THAT far on
|
||||||
|
the spectrum". But it is funny that while finding out who i really am, i met
|
||||||
|
more people who basically had the same issue and a similar biography like mine.
|
||||||
|
Some of them get the autism-diagnosis first, others the ADHD one - since until
|
||||||
|
some time ago those diagnosis were mutually exclusive.
|
||||||
|
|
||||||
|
That's also why many people like me are only really diagnosed as adults, because
|
||||||
|
autism hides many effects of ADHD and vice-versa - depending on which one is
|
||||||
|
currently dominating. It is basically 2 modes: do everything all at once and
|
||||||
|
start everything that grabs your attention - or do a deep dive into a single
|
||||||
|
thing. And the exact opposite: The ADHD part being bored by the autism-project,
|
||||||
|
the autism-part is completely overwhelmed by the ADHD chaos. Both then leading
|
||||||
|
to exhaustion, not being able to do anything .. and basically feeling guilty for
|
||||||
|
the things you did not manage to finish.
|
||||||
|
|
||||||
|
Today i finally found myself. I currently have a great partner (with 3 kids) -
|
||||||
|
and **all** of them have similar issues. Like i said: I best get along with
|
||||||
|
similar people - and also fall in love with those.. and as AuDHD runs in the
|
||||||
|
genes all offspring has a good chance of catching it to varies degrees, too.
|
||||||
|
|
||||||
|
I think the most important thing was the ADHD-Diagnosis - as this enabled me to
|
||||||
|
get metylphenidate to basically get into a "3-4 hours focused as long as the
|
||||||
|
pill works" and total chaos afterwards. This enables me to have productive
|
||||||
|
days/times where i can do all the boring-work that my ADHD-Part wants to sit out
|
||||||
|
and the autism part is overwhelmed from even starting.
|
||||||
|
|
||||||
|
## The early days
|
||||||
|
|
||||||
|
To be continued ...
|
39
Opinions/Don't train your own LLM.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Writing
|
||||||
|
- Drezil
|
||||||
|
- incomplete
|
||||||
|
- Experience
|
||||||
|
- Article
|
||||||
|
title: Don't train your own LLM
|
||||||
|
doi: not yet
|
||||||
|
status: Outline
|
||||||
|
draft: true
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- ML
|
||||||
|
- Opinion
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common reasons to try to train your own LLM
|
||||||
|
|
||||||
|
- Gründe, wieso Leute das wollen - oder eher meinen zu wollen
|
||||||
|
- "Wir vertrauen X nicht"
|
||||||
|
- "Aber unsere Daten sind gaaanz besonders"
|
||||||
|
- "Ich habe das gehört und es klingt gut."
|
||||||
|
- Nachteile
|
||||||
|
- Trainingsdaten?
|
||||||
|
- Trainingskosten?
|
||||||
|
- Rechenkapazität?
|
||||||
|
|
||||||
|
## Is finetuning at least feasible?
|
||||||
|
|
||||||
|
- tl;dr: only under specific circumstances
|
||||||
|
- Netz muss Open-Source oder anders zugänglich sein => Häufig "geistiges
|
||||||
|
Eigentum" oder "secret sauce"
|
||||||
|
- Menge an annotierten Daten? Nur weil das Netz Open Source ist, heißt das
|
||||||
|
nicht, dass die Trainingsdaten es auch sind.
|
||||||
|
|
||||||
|
## Ok, that sounds like shit. What should i do then?
|
||||||
|
|
||||||
|
- Just™ use good prompting - this goes a very long way
|
27
Opinions/Editors.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Drezil
|
||||||
|
- Experience
|
||||||
|
- Tools
|
||||||
|
- Opinion
|
||||||
|
title: Editors
|
||||||
|
status: Outline
|
||||||
|
categories:
|
||||||
|
- Experience
|
||||||
|
- Opinion
|
||||||
|
date: 2020-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
## Editors
|
||||||
|
|
||||||
|
Better said: "neovim is currently the best™ editor" ;)
|
||||||
|
|
||||||
|
### Current Config
|
||||||
|
|
||||||
|
You can find my current Config along with other things in my
|
||||||
|
[gitea snippet-git](https://gitea.dresselhaus.cloud/Drezil/snippets).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Learning Vim in 2014: Vim as Language - Herding Lions](https://benmccormick.org/2014/07/02/062700.html)
|
||||||
|
- [vi - What is your most productive shortcut with Vim? - Stack Overflow](https://stackoverflow.com/questions/1218390/what-is-your-most-productive-shortcut-with-vim/1220118#1220118)
|
40
Opinions/Keyboard-Layout.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Keyboard-Layout
|
||||||
|
tags:
|
||||||
|
- Experience
|
||||||
|
- Opinion
|
||||||
|
categories:
|
||||||
|
- Experience
|
||||||
|
- Opinion
|
||||||
|
date: 2025-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Since around 2006 i basically write only using the
|
||||||
|
[NEO2](https://neo-layout.org)-Layout. There are many advantages that are not
|
||||||
|
obvious to an onlooker right away.
|
||||||
|
|
||||||
|
Don't get me wrong. I still can type `QWERTZ` - just because you learn an
|
||||||
|
additional layout does not mean that you forget everything from before.
|
||||||
|
|
||||||
|
The secret sauce lies in the deeper layers. Especially layer 3 having all the
|
||||||
|
"hard to reach" things like brackets, braces, etc. right on the home row. And
|
||||||
|
the 4th layer is _magic_ for text-navigation. Left hand has the full navigation,
|
||||||
|
right hand has the complete Numpad - even on laptop-keyboards that are lacking
|
||||||
|
those.
|
||||||
|
|
||||||
|
For me as a person having the usual German Keyboard with AltGr this just means:
|
||||||
|
|
||||||
|
- Putting the thumb down on AltGr - it is above there anyway.
|
||||||
|
- Use left hand as normal arrow-keys (that work EVERYWHERE because they are just
|
||||||
|
arrow keys)
|
||||||
|
- Also use Home/End/PgUp/PgDown/…
|
||||||
|
|
||||||
|
Before i always had to switch over or hope that a thing had support for vi-style
|
||||||
|
"hjkl".
|
||||||
|
|
||||||
|
That's why i also prefer [Neovim](./Editors.md) as my primary editor - just not
|
||||||
|
having to touch your mouse at any time for anything is such a godsend :)
|
||||||
|
|
||||||
|
Best thing: If you don't want to switch, there is also a "Neo-QWERTZ"-variant ..
|
||||||
|
where you can just try the deeper layers while not leaving your QWERTZ-layout
|
||||||
|
behind. But i have just seen and never tried it. Your experience may be sub-par.
|
186
Stuff/Bielefeldverschwoerung.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- fun
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- Fun
|
||||||
|
- Archived
|
||||||
|
title: Die Bielefeld-Verschwörung
|
||||||
|
date: 1994-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Kopie des vermutlichen Originals von (vermutlich) Achim Held aus 1994.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Warnung:** Diese Seite enthält Material, von dem SIE nicht wollen, dass es
|
||||||
|
bekannt wird. Speichern Sie diese Seite nicht auf Ihrer lokalen Platte ab, denn
|
||||||
|
sonst sind Sie auch dran, wenn SIE plötzlich bei Ihnen vor der Tür stehen; und
|
||||||
|
das passiert schneller als man denkt. Auch sollten Sie versuchen, alle Hinweise
|
||||||
|
darauf, dass Sie diese Seite jemals gelesen haben, zu vernichten. Tragen Sie
|
||||||
|
diese Seite auf keinen Fall in ihre Hotlist/Bookmarks/etc... ein!
|
||||||
|
|
||||||
|
Vielen Dank für die Beachtung aller Sicherheitsvorschriften.
|
||||||
|
|
||||||
|
## Die Geschichte der Entdeckung
|
||||||
|
|
||||||
|
Vor einigen Jahren fiel es einigen Unerschrockenen zum ersten Mal auf, dass in
|
||||||
|
den Medien immer wieder von einer Stadt namens 'Bielefeld' die Rede war, dass
|
||||||
|
aber niemand jemanden aus Bielefeld kannte, geschweige denn selbst schon einmal
|
||||||
|
dort war. Zuerst hielten sie dies für eine belanglose Sache, aber dann machte es
|
||||||
|
sie doch neugierig. Sie unterhielten sich mit anderen darüber, ohne zu ahnen,
|
||||||
|
dass dies bereits ein Fehler war: Aus heutiger Sicht steht fest, dass jemand
|
||||||
|
geplaudert haben muss, denn sofort darauf wurden SIE aktiv. Plötzlich tauchten
|
||||||
|
Leute auf, die vorgaben, schon einmal in Bielefeld gewesen zu sein; sogar
|
||||||
|
Personen, die vormals noch laut Zweifel geäußert hatten, berichteten jetzt
|
||||||
|
davon, sich mit eigenen Augen von der Existenz vergewissert zu haben - immer
|
||||||
|
hatten diese Personen bei ihren Berichten einen seltsam starren Blick. Doch da
|
||||||
|
war es schon zu spät - die Saat des Zweifels war gesät. Weitere Personen stießen
|
||||||
|
zu der Kerngruppe der Zweifler, immer noch nicht sicher, was oder wem man da auf
|
||||||
|
der Spur war.
|
||||||
|
|
||||||
|
Dann, im Oktober 1993, der Durchbruch: Auf der Fahrt von Essen nach Kiel auf der
|
||||||
|
A2 erhielten vier der hartnäckigsten Streiter für die Aufdeckung der
|
||||||
|
Verschwörung ein Zeichen: Jemand hatte auf allen Schildern den Namen 'Bielefeld'
|
||||||
|
mit orangem Klebeband durchgestrichen. Da wußte die Gruppe: Man ist nicht
|
||||||
|
alleine, es gibt noch andere, im Untergrund arbeitende Zweifler, womöglich über
|
||||||
|
ganz Deutschland verteilt, die auch vor spektakulären Aktionen nicht
|
||||||
|
zurückschrecken. Von da an war uns klar: Wir müssen diese Scharade aufdecken,
|
||||||
|
koste es, was es wolle! Das Ausmaß der Verschwörung
|
||||||
|
|
||||||
|
Der Aufwand, mit dem die Täuschung der ganzen Welt betrieben wird, ist enorm.
|
||||||
|
Die Medien, von denen ja bekannt ist, dass sie unter IHRER Kontrolle stehen,
|
||||||
|
berichten tagaus, tagein von Bielefeld, als sei dies eine Stadt wie jede andere,
|
||||||
|
um der Bevölkerung das Gefühl zu geben, hier sei alles ganz normal. Aber auch
|
||||||
|
handfestere Beweise werden gefälscht: SIE kaufen hunderttausende von Autos,
|
||||||
|
versehen sie mit gefälschten 'BI-'Kennzeichen und lassen diese durch ganz
|
||||||
|
Deutschland fahren. SIE stellen, wie bereits oben geschildert, entlang der
|
||||||
|
Autobahnen große Schilder auf, auf denen Bielefeld erwähnt wird. SIE
|
||||||
|
veröffentlichen Zeitungen, die angeblich in Bielefeld gedruckt werden.
|
||||||
|
Anscheinend haben SIE auch die Deutsche Post AG in Ihrer Hand, denn auch im PLZB
|
||||||
|
findet man einen Eintrag für Bielefeld; und ebenso wird bei der Telekom ein
|
||||||
|
komplettes Ortsnetz für Bielefeld simuliert. Einige Leute behaupten sogar in
|
||||||
|
Bielefeld studiert zu haben und können auch gut gefälschte Diplome u.ä. der
|
||||||
|
angeblich existenten Uni Bielefeld vorweisen. Auch Bundeskanzler Gerhard
|
||||||
|
Schröder behauptet, 1965 das "Westfalen-Kolleg" in Bielefeld besucht zu haben,
|
||||||
|
wie seinem Lebenslauf unter dem Link Bildungsweg zu entnehmen ist.
|
||||||
|
|
||||||
|
Aber auch vor dem Internet machen SIE nicht halt. SIE vergeben Mail-Adressen für
|
||||||
|
die Domain uni-bielefeld.de, und SIE folgen auch den neuesten Trends: SIE bieten
|
||||||
|
im WWW eine ["Stadtinfo über Bielefeld"](https://bielefeld.de) an, sogar mit
|
||||||
|
Bildern; das Vorgarten-Foto, das dem Betrachter als "Botanischer Garten"
|
||||||
|
verkauft werden sollte, ist nach der Entlarvung auf dieser Seite jedoch
|
||||||
|
inzwischen wieder entfernt worden. Aber auch die noch vorhandenen Bilder sind
|
||||||
|
sogar für den Laien als Fotomontagen zu erkennen. Wir sind noch nicht dahinter
|
||||||
|
gekommen, wo der Rechner steht, auf dem die Domains .bielefeld.de und
|
||||||
|
uni-bielefeld.de gefälscht werden; wir arbeiten daran. Inzwischen wurde auch von
|
||||||
|
einem IHRER Agenten - der Täter ist uns bekannt - versucht, diese WWW-Seite zu
|
||||||
|
sabotieren, ich konnte den angerichteten Schaden jedoch zum Glück wieder
|
||||||
|
beheben.
|
||||||
|
|
||||||
|
Ein anonymer Informant, der ganz offensichtlich zu IHNEN zu gehören scheint oder
|
||||||
|
zumindest gute Kontakte zu IHNEN hat, hat mich kürzlich in einer Mail auf die
|
||||||
|
nächste Stufe IHRER Planung hingewiesen: "Ich schätze, spätestens in 10 Jahren
|
||||||
|
wird es heißen: Bielefeld muss Hauptstadt werden." Was das bedeutet, muss ja
|
||||||
|
wohl nicht extra betont werden.
|
||||||
|
|
||||||
|
Die schrecklichste Maßnahme, die SIE ergriffen haben, ist aber zweifelsohne
|
||||||
|
immer noch die Gehirnwäsche, der immer wieder harmlose Menschen unterzogen
|
||||||
|
werden, die dann anschließend auch die Existenz von Bielefeld propagieren. Immer
|
||||||
|
wieder verschwinden Menschen, gerade solche, die sich öffentlich zu ihren
|
||||||
|
Bielefeldzweifeln bekannt haben, nur um dann nach einiger Zeit wieder
|
||||||
|
aufzutauchen und zu behaupten, sie seien in Bielefeld gewesen. Womöglich wurden
|
||||||
|
einige Opfer sogar mit Telenosestrahlen behandelt. Diesen armen Menschen konnten
|
||||||
|
wir bisher nicht helfen. Wir haben allerdings inzwischen einen Verdacht, wo
|
||||||
|
diese Gehirnwäsche durchgeführt wird: Im sogenannten Bielefeld-Zentrum, wobei
|
||||||
|
SIE sogar die Kaltblütigkeit besitzen, den Weg zu diesem Ort des Schreckens von
|
||||||
|
der Autobahn aus mit großen Schildern auszuschildern. Wir sind sprachlos,
|
||||||
|
welchen Einfluß SIE haben.
|
||||||
|
|
||||||
|
Inzwischen sind - wohl auch durch mehrere Berichte in den wenigen nicht von
|
||||||
|
IHNEN kontrollierten Medien - mehr und mehr Leute wachsamer geworden und machen
|
||||||
|
uns auf weitere Aspekte der Verschwörung aufmerksam. So berichtet zum Beispiel
|
||||||
|
Holger Blaschka:
|
||||||
|
|
||||||
|
"Auch der DFB ist in diesen gewaltigen Skandal verwickelt, spielt in der ersten
|
||||||
|
Liga doch ein Verein, den SIE Arminia Bielefeld getauft haben, der innert 2
|
||||||
|
Jahren aus dem Nichts der Amateur-Regionen im bezahlten Fußball auftauchte und
|
||||||
|
jetzt im Begriff ist, sich zu IHRER besten Waffe gegen all die Zweifler zu
|
||||||
|
entwickeln. Den Gästefans wird vorgetäuscht mit ihren Bussen nach Bielefeld zu
|
||||||
|
kommen, wo sie von IHNEN abgefangen werden, um direkt ins Stadion geleitet zu
|
||||||
|
werden. Es besteht keine Chance sich die Stadt näher anzuschauen, und auch die
|
||||||
|
Illusion des Heimpublikums wird durch eine größere Menge an bezahlten Statisten
|
||||||
|
aufrechterhalten. Selbst ehemalige Top-Spieler, die Ihren Leistungszenit bei
|
||||||
|
weitem überschritten haben, werden zu diesem Zweck von IHNEN mißbraucht. Mit
|
||||||
|
genialen Manövern, u.a. vorgetäuschten Faustschlägen und Aufständen gegen das
|
||||||
|
Präsidium eines baldigen Drittligisten wurde von langer Hand die wohl
|
||||||
|
aufwendigste Täuschung aller Zeiten inszeniert. Es gibt noch mehr Beweise: Das
|
||||||
|
sich im Rohbau befindende Stadion, das gefälschte und verpanschte Bier und nicht
|
||||||
|
zuletzt die Tatsache, dass dieser Verein nur einen Sponsor hat. SIE, getarnt als
|
||||||
|
Modefirma Gerry Weber."
|
||||||
|
|
||||||
|
## Was steckt dahinter?
|
||||||
|
|
||||||
|
Dies ist die Frage, auf die wir auch nach jahrelangen Untersuchungen immer noch
|
||||||
|
keine befriedigende Antwort geben können. Allerdings gibt es einige Indizien,
|
||||||
|
die auf bestimmte Gruppierungen hinweisen:
|
||||||
|
|
||||||
|
- Es könnte eine Gruppe um den Sternenbruder und Weltenlehrer Ashtar Sheran
|
||||||
|
dahinterstecken, die an der Stelle, an der Bielefeld liegen soll, ihre Landung
|
||||||
|
vorbereiten, die - einschlägiger Fachliteratur zufolge - kurz bevorsteht. Zu
|
||||||
|
dieser Gruppe sollen auch Elvis und Kurt Cobain gehören, die beide - vom
|
||||||
|
schwedischen Geheimdienst gedeckt - noch am Leben sind.
|
||||||
|
- An der Stelle, an der Bielefeld liegen soll, hält die CIA John F. Kennedy seit
|
||||||
|
dem angeblichen Attentat versteckt, damit er nichts über die vorgetäuschte
|
||||||
|
Mondlandung der NASA erzählen kann. Inwieweit die Reichsflugscheibenmacht von
|
||||||
|
ihrer Mond- oder Marsbasis aus da mitspielt, können wir nicht sagen, da alle
|
||||||
|
Beweise beim Abschuß der schwer bewaffneten Marssonde Observer vernichtet
|
||||||
|
wurden. Informationen hierüber besitzt vielleicht der Vatikan, der seit den
|
||||||
|
50er Jahren regelmäßig mit tachyonenangetriebenen Schiffen zum Mars fliegt.
|
||||||
|
- Der MOSSAD in Zusammenarbeit mit dem OMEGA-Sektor planen an dieser Stelle die
|
||||||
|
Errichtung eines geheimen Forschungslabors, weil sich genau an diesem Ort zwei
|
||||||
|
noch nicht dokumentierte Ley-Linien kreuzen. Dort könnte auch der Jahrtausende
|
||||||
|
alte Tunnel nach Amerika und Australien (via Atlantis) seinen Eingang haben.
|
||||||
|
Wichtige Mitwisser, namentlich Uwe Barschel und Olof Palme, wurden von den mit
|
||||||
|
dem MOSSAD zusammenarbeitenden Geheimdiensten, darunter der Stasi und der
|
||||||
|
weniger bekannten 'Foundation', frühzeitig ausgeschaltet.
|
||||||
|
- An der Stelle liegt die Höhle eines der schlafenden Drachen aus dem Vierten
|
||||||
|
Zeitalter, die auf das Erwachen der Magie am 24. Dezember 2011 (siehe hierzu
|
||||||
|
den Maya-Kalender) warten. Beschützt wird diese Stelle von den Rittern des
|
||||||
|
Ordenskreuzes AAORRAC, die sich inzwischen mit der Herstellung von
|
||||||
|
programmiertem Wasser beschäftigen - nach einen Rezept, das sie unter brutaler
|
||||||
|
Folter von Ann Johnson bekommen haben. Diese hatte es bekanntlich von hohen
|
||||||
|
Lichtwesen aus dem All erhalten, um die Menschheit vor außerirdischen
|
||||||
|
Implantaten bis Stufe 3 zu schützen.
|
||||||
|
|
||||||
|
## Was können wir tun?
|
||||||
|
|
||||||
|
Zum einen können wir alle an den Bundestag, das Europaparlament und die UNO
|
||||||
|
schreiben, um endlich zu erreichen, dass SIE nicht mehr von den Politikern
|
||||||
|
gedeckt werden. Da aber zu befürchten ist, dass SIE die Politik - so wie auch
|
||||||
|
das organisierte Verbrechen und die großen Weltreligionen - unter Kontrolle
|
||||||
|
haben, sind die Erfolgschancen dieses Weges doch eher zweifelhaft.
|
||||||
|
|
||||||
|
Eine weitere Möglichkeit besteht darin, dass sich alle Bielefeldzweifler treffen
|
||||||
|
und gemeinsam durch transzendentale Meditation (TM) soviel positive Ausstrahlung
|
||||||
|
erzeugen, dass der Schwindel auffliegt. Eine ähnliche Vorgehensweise hat in
|
||||||
|
Washington, D.C. für eine Senkung der Verbrechensrate um über 20% gesorgt.
|
||||||
|
Besonders effektiv ist dies im Zusammenwirken mit Hopi-Kerzen im Ohr und
|
||||||
|
Yogischem Schweben.
|
||||||
|
|
||||||
|
Ab und zu nimmt in einer der eigentlich von IHNEN kontrollierten Zeitungen ein
|
||||||
|
Redakteur allen Mut zusammen und riskiert es, in einer der Ausgaben zumindest
|
||||||
|
andeutungsweise auf die Verschwörung hinzuweisen. So wurde in der FAZ Bielefeld
|
||||||
|
als "Die Mutter aller Un-Städte" bezeichnet, und die taz überschrieb einen
|
||||||
|
Artikel mit "Das Bermuda-Dreieck bei Bielefeld". Auf Nachfrage bekommt man dann
|
||||||
|
natürlich zu hören, das habe man alles ganz anders gemeint, bei der taz hieß es
|
||||||
|
sogar, es hätte in Wirklichkeit "Bitterfeld" heißen sollen, aber für einen
|
||||||
|
kurzen Moment wurden die Leser darauf aufmerksam gemacht, dass mit Bielefeld
|
||||||
|
etwas nicht stimmt. An dem Mut dieser Redakteure, über deren weiteres Schicksal
|
||||||
|
uns leider nichts bekannt ist, sollten wir uns alle ein Beispiel nehmen.
|
||||||
|
|
||||||
|
Das, was wir alle aber für uns im kleinen tun können, ist folgendes: Kümmert
|
||||||
|
euch um die bedauernswerten Opfer der Gehirnwäsche, umsorgt sie, macht ihnen
|
||||||
|
behutsam klar, dass sie einer Fehlinformation unterliegen. Und, bekennt euch
|
||||||
|
alle immer offen, damit SIE merken, dass wir uns nicht länger täuschen lassen:
|
||||||
|
**Bielefeld gibt es nicht!!!**
|
216
Uni/Lernerfolg_an_der_Uni.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Tutorial
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
code-overflow: wrap
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- Experience
|
||||||
|
- Uni
|
||||||
|
title: Wie lerne ich richtig an der Uni?
|
||||||
|
date: 2015-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Dies ist eine gute Frage. Da ich im laufe der Zeit einige Antworten gesammelt
|
||||||
|
habe, wollte ich diese mal hier niederschreiben. Vorweg eine Warnung: **All das
|
||||||
|
hier spiegelt nur meine persönlichen Erfahrungen aus Gesprächen wieder. Es kann
|
||||||
|
sein, dass die z.B. für euren Fachbereich nicht gilt.** Da wir das nun aus dem
|
||||||
|
Weg haben, geht es auch gleich los.
|
||||||
|
|
||||||
|
## Uni ist nicht Schule
|
||||||
|
|
||||||
|
Einige mögen sagen: "duh!", aber es ist erschreckend, wie viele Leute meinen,
|
||||||
|
dass ihnen die Uni etwas schuldet oder das Dozenten und Tutoren dafür
|
||||||
|
verantwortlich sind, dass man hier etwas lernt. Studium ist eine komplett
|
||||||
|
freiwillige Veranstaltung. Man kann jederzeit sagen: "Passt mir nicht. Ich
|
||||||
|
gehe." An der Uni wird erwartet, dass man sich ggf. einarbeitet, wenn man etwas
|
||||||
|
nicht weiss; dass man Sekundärliteratur fragt (z.B. in Mathe auch mal in Bücher
|
||||||
|
schaut um eine andere Erklärung zu bekommen, als der Prof an die Tafel
|
||||||
|
geklatscht hat).
|
||||||
|
|
||||||
|
## Etwas Lerntheorie
|
||||||
|
|
||||||
|
Es gibt einen sehr schönen [Talk](https://www.youtube.com/watch?v=Z8KcCU-p8QA)
|
||||||
|
von Edwand Kmett in dem er über seine Erfahrungen berichtet. Kurzum: Man lernt
|
||||||
|
durch stete Wiederholung. Und der beste Moment etwas zu wiederholen ist, kurz
|
||||||
|
bevor man es vergisst. Das stimmt ziemlich genau mit meiner Erfahrung überein.
|
||||||
|
|
||||||
|
### Auswendig lernen
|
||||||
|
|
||||||
|
Grade die oben genannte Theorie steht beim Auswendiglernen im Vordergrund. Wenn
|
||||||
|
man etwas langfristig auswendig lernen will (Fremdsprachen, etc.), dann gibt es
|
||||||
|
hierzu Software, die herausfindet, wann es der beste Zeitpunkt ist, dich wieder
|
||||||
|
abzufragen: [Anki](http://ankisrs.net/) gibt es für jede Platform kostenlos
|
||||||
|
(außer iPhone - hier 25\$, weil Apple so viel Geld für das einstellen im
|
||||||
|
AppStore haben will). Anki ist dazu gedacht, dass man zu jedem Thema einen
|
||||||
|
Stapel hat (z.b. Klausurfragen, Sprachen, ...) und jeden Tag lernt. Nach einiger
|
||||||
|
Zeit wird die vorhersage der Lernzeit ziemlich genau. Anfangs beantwortet man
|
||||||
|
noch viele Fragen täglich, aber je häufiger man die Antworten kennt, desto
|
||||||
|
weiter hinten landen sie im Stapel. Schlussendlich kommt dieselbe Frage dann nur
|
||||||
|
noch 1x/Monat oder noch seltener.
|
||||||
|
|
||||||
|
Ich benutze dies insbesondere zum Auswendiglernen von Fakten, Formeln,
|
||||||
|
Fachbegriffen etc. Bei Mathe bietet sich zum Beispiel an einen Stapel mit allen
|
||||||
|
Definitionen zu haben; in der Biologie eine Liste der Schema und Kreisläufe etc.
|
||||||
|
|
||||||
|
Man kann auch einen Hardcore-Lernmarathon machen. Meine letzten beiden Klausuren
|
||||||
|
waren nur auf "bestehen" - also ohne Note. Ich habe mir eine alte Klausur
|
||||||
|
organisiert (mehr genaues unten) und dann daraus Karten erstellt. Dies hat nur
|
||||||
|
wenige Stunden gedauert (2-3 verteilt auf 2 Tage). Damit habe ich dann am Tag
|
||||||
|
vor der Klausur 2x gelernt (1x nach dem Aufstehen, 1x vorm schlafengehen;
|
||||||
|
jeweils nach 30 Minuten hatte ich alle Fragen min. 1x korrekt beantwortet). Am
|
||||||
|
Morgen der Klausur hab ich die Fragen vor dem Aufstehen noch einmal durchgemacht
|
||||||
|
(wieder 25-30 min), habe mir zur Klausur fertig gemacht und bin 30 Min vor der
|
||||||
|
Klausur die Fragen nochmals durchgegangen (15-30 min), aber konnte sie
|
||||||
|
mittlerweile alle auswendig. Insgesamt habe ich mit Anki so für die Klausur
|
||||||
|
effektiv 2h gelernt (+2-3h für das erstellen der Karten), habe die Klausur
|
||||||
|
geschrieben und mit einer 3.0 bestanden (also wäre 3.0 gewesen, wenn es nicht
|
||||||
|
unbenotet gewesen wäre). Kommilitonen, die sich (nach eigener Aussage) 1-2
|
||||||
|
Wochen auf die Klausur vorbereitet haben und eine Note wollten, schnitten
|
||||||
|
teilweise schlechter ab (viele aber auch viel besser).
|
||||||
|
|
||||||
|
### Methodik lernen
|
||||||
|
|
||||||
|
Im Gegensatz zum plumpen auswendig lernen gibt es dann auch Anforderungen, wo es
|
||||||
|
darum geht Methoden und Anwendungen zu verstehen. Inbesondere ist dies in
|
||||||
|
Vorbereitung auf z.B. mündliche Prüfungen der Fall. Hier steht eher die Theorie
|
||||||
|
im Vordergrund.
|
||||||
|
|
||||||
|
Um solche Konzepte zu verstehen braucht es leider Zeit. Hier hilft kein
|
||||||
|
48h-Lernmarathon um das "mal eben" auf die Kette zu kriegen. Am besten bereitet
|
||||||
|
man sich das gesamte Semester über vor (haha! Als ob! :p). Das "Geheimnis" hier
|
||||||
|
liegt in einer Kombination der Ansätze. Zum einen muss man natürlich verstehen,
|
||||||
|
worum es geht. Hier hilft es Definitionen und Fachbegriffe z.B. mit Anki zu
|
||||||
|
lernen. Allerdings muss man sich zusätzlich noch nach jeder(!) Vorlesung
|
||||||
|
hinsetzen und versuchen den Inhalt zu verdauen. Dies können nur 10 Minuten sein
|
||||||
|
oder auch 2h. Hier kommen dann Dinge zum Tragen, wie Sekundärliteratur,
|
||||||
|
Wikipedia, Google, ... Man muss die Zusammenhänge einmal verstehen - da kommt
|
||||||
|
man nicht drumherum. ABER: Unser Gehirn arbeitet Assoziativ. Zusammenhänge sind
|
||||||
|
meist logisch oder krass widersprüchlich. Hieraus kann man dann z.B.
|
||||||
|
"Stichwortketten" bauen, von denen man nur das erste auswendig lernt und von da
|
||||||
|
aus sich an den Rest "erinnert".
|
||||||
|
|
||||||
|
Kleines Beispiel aus der Welt der Mathematik:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
Vektorraum -> Ist zu einer Basis definiert
|
||||||
|
-> Basis ist die größtmögliche Zahl lin. unabh. Vektoren. Lin. Hülle der Basis ist der VR
|
||||||
|
-> Lin. Hülle ist jede Lin.-Komb. von Vektoren
|
||||||
|
-> Hat eine Vektoraddition und skalare Multiplikation
|
||||||
|
-> Wird über einem Körper aufgespannt
|
||||||
|
-> Körper sind 2 abelsche Gruppen mit Distributivgesetz
|
||||||
|
-> abelsche Gruppe ist Menge mit K.A.I.N.
|
||||||
|
-> ....
|
||||||
|
```
|
||||||
|
|
||||||
|
So kann man sich über 5-6 Stichwörter fast am gesamten Stoff der Vorlesung
|
||||||
|
entlanghangeln und merkt schnell, wo es hakt. Hier kann man dann nochmal gezielt
|
||||||
|
nachhaken. Auch kann man bei so einer Struktur aus jedem "a -> b -> c"
|
||||||
|
Anki-Karten machen mit "a" auf der Vorderseite, "b" auf der Rückseite bzw. "b"
|
||||||
|
auf der Vorderseite und "c" auf der Rückseite und so gezielt diese "Ketten"
|
||||||
|
trainieren. Grade in einer mündlichen Prüfung hangeln sich Prüfer ebenfalls an
|
||||||
|
diesen Ketten entlang.
|
||||||
|
|
||||||
|
## Vorbereiten auf eine Klausur
|
||||||
|
|
||||||
|
- Herausfinden, um was für eine Art von Klausur es sich handelt
|
||||||
|
- Ankreuzklausur?
|
||||||
|
- Auswendiglern-Klausur?
|
||||||
|
- Praktische Klausur (z.b. fast 1:1 Übungsaufgaben, feste Schema, ..)?
|
||||||
|
- Open-Book?
|
||||||
|
- Annotation von Grafiken?
|
||||||
|
- Klausuren von der Fachschaft organisieren
|
||||||
|
- Falls keine Vorhanden: Altfachschaftler fragen, wie die Klausur bei ihnen
|
||||||
|
war
|
||||||
|
- Neue Klausur mit in die FS bringen, falls möglich (z.b. schreiend rausrennen
|
||||||
|
und Klausur dabei mitnehmen, bevor man offiziell registriert wurde)
|
||||||
|
|
||||||
|
Je nach Klausurtyp dann mit Anki stumpf Karten machen und auswendig lernen (z.b.
|
||||||
|
Ankreuzklausur, Grafik-annotations-Klausur, ..) oder Übungsaufgaben/Altklausuren
|
||||||
|
durchrechnen
|
||||||
|
|
||||||
|
## Vorbereiten auf eine mündliche Prüfung
|
||||||
|
|
||||||
|
- Protokolle aus der Fachschaft organisieren
|
||||||
|
- Häufig gegen Pfand, dass man bei Abgabe eines Protokolls wieder bekommt
|
||||||
|
- Wenn keins vorhanden für die nachfolgede Generation eins ausfüllen
|
||||||
|
|
||||||
|
Wenn ihr einen Reihe von Protokollen vorliegen habt, dann schreibt alle Fragen
|
||||||
|
heraus und notiert, wie häufig diese Frage gestellt wurde. So findet ihr heraus,
|
||||||
|
auf welche Punkte der Prüfer besonders Wert legt (z.B. häufig sein eigenes
|
||||||
|
Forschungsfeld). Diese Fragen dann restlos klären und zu Anki-Karten
|
||||||
|
verarbeiten. Das reicht meistens für ein Bestehen. Wenn ihr auf eine gute Note
|
||||||
|
wert legt, dann solltet ihr auch noch die Vorlesung, wie im Bereich "Methodik
|
||||||
|
lernen" erwähnt, nacharbeiten. Insbesondere helfen hier die Assoziationsketten
|
||||||
|
weiter den Stoff auch in der Prüfung in der richtigen Reihenfolge abzurufen.
|
||||||
|
Vielleicht erkennt ihr solche Ketten schon aus den Prüfungsprotokollen und könnt
|
||||||
|
euch ausmalen, wie man z.b. von da aus auf andere Themen der Vorlesung kommt
|
||||||
|
(die z.b. neu sind oder überarbeitet wurden).
|
||||||
|
|
||||||
|
### Unterschiede mündliche Bachelor/Master-Prüfungen
|
||||||
|
|
||||||
|
Einige Dozenten machen unterschiedliche Anforderungen, ob sie einen Bachelor
|
||||||
|
oder einen Master-Studenten prüfen. Abgesehen von der anderen Prüfungszeit
|
||||||
|
(15-30min bei bachelor, 25-45 bei Master) ist hier auch das Vorgehen anders. Bei
|
||||||
|
einem Bachelor wird klassischerweise alles oberflächlich abgefragt und nur wenig
|
||||||
|
in die Tiefe gegangen. Bei einem Master wir nur noch stichpunktartig gefragt,
|
||||||
|
dafür aber bis ins Detail.
|
||||||
|
|
||||||
|
Beispiel: Ich hatte eine mündliche Masterprüfung, bei der in der Vorlesung 7
|
||||||
|
verschiedene Themen behandelt wurden. In der Prüfung wurden dann nur die
|
||||||
|
Themenübersicht abgefragt und bei 2 Themen komplett in die Tiefe gegangen -
|
||||||
|
inkl. Formeln, Bedeutung, Übertragung auf in der Vorlesung nicht angesprochene
|
||||||
|
Aspekte etc. Die anderen 5 Themen kamen nicht dran. Bei meinen Bachelorprüfungen
|
||||||
|
war das eher umgekehrt: Hier wurde sich grob an der Vorlesung entlang gehangelt
|
||||||
|
und zumindest alles einmal kurz angetestet, ob die zentralen Inhalte der
|
||||||
|
Vorlesung verstanden wurden.
|
||||||
|
|
||||||
|
Dies hat häufig auch damit zu tun, dass man im Bachelor eher Grundlagen hört und
|
||||||
|
somit ein grobes Verständnis aller Dinge wichtig ist, während im Master auf die
|
||||||
|
Aneignung von Tiefenwissen ankommt.
|
||||||
|
|
||||||
|
## Prüfungsangt
|
||||||
|
|
||||||
|
Zu guter Letzt noch ein paar Worte zum Thema Prüfungsangst. Es ist normal, dass
|
||||||
|
man vor einer Prüfung angespannt ist. Es ist nicht normal, wenn die Anspannung
|
||||||
|
so ausartet, dass man sich übergibt, Krämpfe bekommt oder ähnlich starke
|
||||||
|
Symptome zeigt. Ich leide selbst an solchen Problemen und habe mich schon
|
||||||
|
mehrfach vor Prüfungen übergeben. Eine klassische Konfrontationstherapie
|
||||||
|
funktioniert aufgrund der Seltenheit der Prüfungen nicht oder nur sehr schwer.
|
||||||
|
Ich habe mich an meinen Arzt gewendet und habe nun genau für solche Situationen
|
||||||
|
ein Medikament. 1-2h vor einer Prüfung nehme ich das und komme in einen
|
||||||
|
komischen Zustand. Ich merke zwar noch, dass ich Angespannt bin und eigentlich
|
||||||
|
Angst hätte, aber es "stört" mich nicht wirklich. Es versetzt mich nicht in
|
||||||
|
Panik oder sonstwas. Es schaltet mein Gehirn nicht aus oder hat andere negative
|
||||||
|
Effekte. Natürlich geht das auch mit Nachteilen einher: ein paar Tage keinen
|
||||||
|
Alkohol, kein Auto fahren, etc. - Aber meist ist das ja nur 2-3x/Semester der
|
||||||
|
Fall. Wenn man nicht so stark betroffen ist, dann ist davon allerdings
|
||||||
|
abzuraten. Das Medikament gleicht die Panik durch Gelassenheit aus - wenn man
|
||||||
|
keine Panik hat, dann wird man hierdurch so "gelassen" dass man mehrere Stunden
|
||||||
|
einschläft - was in einer Prüfung vielleicht nicht ganz so gut ist ;)
|
||||||
|
|
||||||
|
Es gibt auch zahlreiche Regularien und Rechtsansprüche, die ihr bei sowas habt.
|
||||||
|
Ihr habt zum Beispiel (sofern ein (Amts?-)Arzt eine Prüfungsangst bestätigt hat)
|
||||||
|
Anspruch auf mehr Prüfungszeit, die Prüfung alleine abzulegen (z.b. bei einem
|
||||||
|
Mitarbeiter, während andere im Hörsaal schreiben), eine mündliche durch eine
|
||||||
|
schriftliche zu tauschen (oder umgekehrt), etc. Das kann man individuell mit dem
|
||||||
|
Prüfer absprechen. Ich weiss nicht, wie das in anderen Fakultäten läuft - aber
|
||||||
|
in der Technischen Fakultät hat fast jeder Prüfer dafür volles Verständnis
|
||||||
|
(einige litten sogar früher selbst an sowas).
|
||||||
|
|
||||||
|
Die kostenlose psychologische Beratung an der Uni (aka. "Das rote Sofa" im X)
|
||||||
|
bietet hier auch Hilfestellung bei und vermittelt in schwereren Fällen auch
|
||||||
|
gleich noch eine Therapie/Ärzte. Hier kann man z.b. Prüfungssimulationen
|
||||||
|
abhalten oder sich Hilfe holen, wenn ein Dozent sich querstellt. Die Mitarbeiter
|
||||||
|
begleiten einen z.B. auch zu einer Prüfung (nach Absprache mit dem
|
||||||
|
Veranstalter), falls das hilft, etc.
|
||||||
|
|
||||||
|
Es ist keine Schande so ein Problem zu haben und es gibt genug, die sich damit
|
||||||
|
rumschlagen. Aber man ist hier an der Uni auch nicht alleine damit. Es gibt
|
||||||
|
zahlreiche Hilfsangebote.
|
||||||
|
|
||||||
|
## Schlusswort
|
||||||
|
|
||||||
|
Viel Erfolg bei euren Prüfungen. Falls euch dieser Artikel geholfen hat oder ihr
|
||||||
|
noch Anregungen/Verbessenguswünsche habt, schreibt mir einfach.
|
748
Writing/Obsidian-RAG.qmd
Normal file
@ -0,0 +1,748 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Writing
|
||||||
|
aliases:
|
||||||
|
- "RAG für eine Obsidian-Wissensdatenbank: Technische Ansätze"
|
||||||
|
cssclasses:
|
||||||
|
- table-wide
|
||||||
|
- table-wrap
|
||||||
|
authors:
|
||||||
|
- name: GPT-4.5
|
||||||
|
url: https://chatgpt.com
|
||||||
|
affiliation:
|
||||||
|
- name: OpenAI
|
||||||
|
url: https://openai.com
|
||||||
|
- name: cogito-v1-preview
|
||||||
|
url: https://www.deepcogito.com/research/cogito-v1-preview
|
||||||
|
affiliation:
|
||||||
|
- name: DeepCogito
|
||||||
|
url: https://www.deepcogito.com
|
||||||
|
- name: Claude 3.7 Sonnet
|
||||||
|
url: https://claude.ai
|
||||||
|
affiliation:
|
||||||
|
- name: Antrhopic
|
||||||
|
url: https://www.anthropic.com
|
||||||
|
- name: Nicole Dresselhaus
|
||||||
|
affiliation:
|
||||||
|
- name: Humboldt-Universität zu Berlin
|
||||||
|
url: https://hu-berlin.de
|
||||||
|
orcid: 0009-0008-8850-3679
|
||||||
|
date: 2025-04-24
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- RAG
|
||||||
|
- ML
|
||||||
|
fileClass: authored
|
||||||
|
lang: de
|
||||||
|
linter-yaml-title-alias:
|
||||||
|
"RAG für eine Obsidian-Wissensdatenbank: Technische Ansätze"
|
||||||
|
title: "RAG für eine Obsidian-Wissensdatenbank: Technische Ansätze"
|
||||||
|
bibliography:
|
||||||
|
- obsidian-rag.bib
|
||||||
|
citation-style: springer-humanities-brackets
|
||||||
|
image: ../thumbs/writing_obsidian-rag.png
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hintergrund und Zielsetzung
|
||||||
|
|
||||||
|
Der Nutzer verfügt über eine Obsidian-Wissensdatenbank, in der Markdown-Dateien
|
||||||
|
mit **typisierten** Inhalten (FileClasses wie `Person`, `Projekt`, `Deliverable`
|
||||||
|
etc.) verwaltet werden. Die Notizen enthalten strukturierte **YAML-Metadaten**
|
||||||
|
(unterstützt durch Plugins wie _Metadata Menu_) und sind durch viele
|
||||||
|
**Wiki-Links** miteinander vernetzt. Standardisierte Templates (via _Templater_)
|
||||||
|
sorgen dafür, dass z.B. Personenseiten immer ähnliche Felder (Name, ORCID, etc.)
|
||||||
|
aufweisen.
|
||||||
|
|
||||||
|
**Ziel** ist es, mithilfe eines Language Models (LLM) wiederkehrende Aufgaben zu
|
||||||
|
erleichtern, zum Beispiel: automatisch YAML-Felder ausfüllen (etwa fehlende
|
||||||
|
ORCID iDs bei Personen ergänzen), neue Entitätsseiten anhand von Templates
|
||||||
|
befüllen oder sinnvolle Verlinkungen zwischen Notizen vorschlagen. Dabei reicht
|
||||||
|
ein tägliches Neu-Einlesen der Obsidian-Daten (via Cronjob o.Ä.) aus – eine
|
||||||
|
Echtzeit-Synchronisation ist optional. Die Obsidian-internen Wikilinks
|
||||||
|
(`[[...]]`) müssen im LLM-Ausgabeformat nicht unbedingt klickbar sein (es
|
||||||
|
genügt, wenn sie referenziert werden).
|
||||||
|
|
||||||
|
Um diese Funktionen umzusetzen, bieten sich verschiedene **technische Ansätze**
|
||||||
|
an. Im Folgenden werden fünf Optionen untersucht: (1) Nutzung eines
|
||||||
|
**Vektorspeichers** für semantische Suche, (2) Aufbau eines **Knowledge Graph**
|
||||||
|
aus den Notizen, (3) eine **Hybrid-Lösung** aus Graph und Vektor, (4) Extraktion
|
||||||
|
& Normalisierung der **YAML-Metadaten** und (5) existierende **Tools/Workflows**
|
||||||
|
zur Automatisierung. Jede Option wird mit Funktionsweise, Vorund Nachteilen,
|
||||||
|
Aufwand, Integrationsmöglichkeiten (insb. mit lokalen LLMs wie LLaMA, Deepseek,
|
||||||
|
Cogito etc.) sowie konkreten Tool-Empfehlungen dargestellt.
|
||||||
|
|
||||||
|
## 1. Vektorbasierter Ansatz: Semantic Search mit Embeddings
|
||||||
|
|
||||||
|
### Prinzip
|
||||||
|
|
||||||
|
Alle Markdown-Notizen (bzw. deren Inhalt) werden in kleinere Chunks zerlegt und
|
||||||
|
durch einen Embedding-Modell in hochdimensionale Vektoren umgewandelt. Diese
|
||||||
|
Vektoren werden in einem **Vektorstore** (wie ChromaDB oder Weaviate)
|
||||||
|
gespeichert. Bei Anfragen des LLM (z.B. _"Welche Projekte hat Person X?"_ oder
|
||||||
|
_"Erstelle eine neue Organisation XYZ basierend auf ähnlichen Einträgen"_),
|
||||||
|
können mittels **ähnlichkeitssuche** semantisch passende Notiz-Abschnitte
|
||||||
|
abgerufen und dem LLM als Kontext mitgegeben werden (Retrieval-Augmented
|
||||||
|
Generation).
|
||||||
|
|
||||||
|
### Implementierung
|
||||||
|
|
||||||
|
In der Praxis ließe sich z.B. ein Workflow mit **Ollama** + **Nomic
|
||||||
|
Embeddings** + **Chroma**^[Alle diese Teile laufen bereits individuell in der
|
||||||
|
Arbeitsgruppe bzw. werden schon genutzt.] aufbauen. Ollama stellt ein lokales
|
||||||
|
LLM-Serving bereit und bietet auch eine API für Embeddins
|
||||||
|
[@ollama_chroma_cookbook]. Man könnte ein spezialisiertes Embeddin-Modell wie
|
||||||
|
`nomic-embed-text` verwenden, welches kompakte 1024-dimensionale Textvektoren
|
||||||
|
liefert [@ollama_chroma_cookbook]. Die Notizen des Obsidian Vault würden per
|
||||||
|
Skript täglich eingelesen, in Sinnabschnitte (Chunks) aufgeteilt (z.B. nach
|
||||||
|
Überschriften oder einer festen Token-Länge) und über Ollamas Embedding-API in
|
||||||
|
Vektoren umgewandelt [@ollama_chroma_cookbook]. Diese Vektoren speichert man in
|
||||||
|
einer lokalen DB wie Chroma. Anfragen an das LLM werden dann zunächst an den
|
||||||
|
Vektorstore gestellt, um die relevantesten Notiz-Abschnitte zu finden, welche
|
||||||
|
dann zusammen mit der eigentlichen Frage an das LLM gegeben werden (klassischer
|
||||||
|
RAG-Pipeline). Dieses Verfahren ist vergleichbar mit dem _Smart Connections_
|
||||||
|
Obsidian-Plugin: Dort wird ebenfalls ein "Text Embedding Model" auf den Vault
|
||||||
|
angewendet, um zu einer Nutzerfrage automatisch thematisch passende Notizen zu
|
||||||
|
finden und dem LLM bereitzustellen [@smart_connections_plugin]. So konnte im
|
||||||
|
Beispiel ein lokales LLaMA-basiertes Modell Fragen zum eigenen Vault korrekt
|
||||||
|
beantworten, indem es zuvor den passenden Ausschnitt (hier: eine
|
||||||
|
Styleguide-Notiz) über Embeddings gefunden hatte [@smart_connections_plugin].
|
||||||
|
|
||||||
|
### Integration mit lokalen LLMs
|
||||||
|
|
||||||
|
Ein Vorteil dieses Ansatzes ist, dass er schon heute mit lokalen
|
||||||
|
Open-Source-LLMs funktioniert. Beispielsweise ließ sich in _Smart Connections_
|
||||||
|
ein lokal gehostetes LLaMA-Model (3B Instruct) via text-generation-webui
|
||||||
|
einbinden [@smart_connections_plugin]. Alternativ kann man auch
|
||||||
|
_LLM-as-a-service_ Tools wie **Ollama** nutzen, um ein Modell wie Llama 2
|
||||||
|
bereitzustellen. Die Open-Source-Tools **LangChain** oder **LlamaIndex** bieten
|
||||||
|
Module, um Vektorstores anzubinden und mit LLM-Abfragen zu kombinieren – dies
|
||||||
|
kann man auch mit lokal eingebundenen Modellen (z.B. über LlamaCpp oder GPT4All)
|
||||||
|
verwenden. Zahlreiche fertige Projekte demonstrieren dieses Vorgehen: z.B.
|
||||||
|
_privateGPT_ kombiniert LangChain, GPT4All (lokales LLM) und Chroma, um komplett
|
||||||
|
offline Fragen über lokale Dateien zu beantworten
|
||||||
|
[@second_brain_assistant_with_obsidian]. Auch **Khoj** verfolgt einen ähnlichen
|
||||||
|
Pfad: Es indexiert den Vault und erlaubt semantische **Natürliche Sprache
|
||||||
|
Suche** über Markdown-Inhalte sowie _"ähnliche Notizen finden"_ [@khoj_plugin].
|
||||||
|
|
||||||
|
### Leistung
|
||||||
|
|
||||||
|
Dank moderner Embedding-Modelle können semantisch ähnliche Inhalte gefunden
|
||||||
|
werden, selbst wenn die Schlagwörter nicht exakt übereinstimmen. Das löst das in
|
||||||
|
Obsidian bekannte Problem, dass die eingebaute Suche nur exakte Worttreffer
|
||||||
|
findet [@supercharging_obsidian_search]. Der Ansatz skaliert auch auf größere
|
||||||
|
Wissensbasen; Vektordatenbanken wie Weaviate oder Chroma sind für zehntausende
|
||||||
|
Einträge ausgelegt. Eine tägliche Aktualisierung ist machbar, da nur
|
||||||
|
neue/geänderte Notizen re-embedded werden müssen.
|
||||||
|
|
||||||
|
### Nachteile und Aufwand
|
||||||
|
|
||||||
|
Die Einrichtung erfordert mehrere Komponenten. Man benötigt Pipeline-Schritte
|
||||||
|
für das Chunking, Embedding und das Handling des Vektorstores – dies bedeutet
|
||||||
|
anfängliche Komplexität und Rechenaufwand [@supercharging_obsidian_search].
|
||||||
|
Insbesondere das Generieren der Embeddings kann bei großen Vaults zeitund
|
||||||
|
speicherintensiv sein (je nach Modell und Hardware)
|
||||||
|
[@supercharging_obsidian_search]. Laufende Kosten sind bei rein lokaler
|
||||||
|
Verarbeitung allerdings kein Thema außer CPU/GPU-Last. Ein potenzieller Nachteil
|
||||||
|
ist, dass rein embeddings-basierte Suche keine **strukturierte** Abfrage erlaubt
|
||||||
|
– das Modell findet zwar thematisch passende Textpassagen, aber um z.B. **eine
|
||||||
|
bestimmte Eigenschaft** (wie eine fehlende ORCID) gezielt abzufragen, müsste man
|
||||||
|
dennoch im Text suchen oder zusätzliche Logik anwenden. Das LLM kann aus den
|
||||||
|
gefundenen Texten zwar implizit Fakten entnehmen, hat aber kein explizites
|
||||||
|
Wissen über die Datenstruktur. Zudem können irrelevante Kontextstücke
|
||||||
|
eingebunden werden, wenn das semantische Matching fehlerhaft ist (dies erfordert
|
||||||
|
ggf. Feintuning der Chunk-Größe oder Filtern per Dateityp/-klasse)^[Und diese
|
||||||
|
Nachteile machen dies zu einem Deal-Breaker. Gerade in Tabellen oder
|
||||||
|
Auflistungen kann der Attention-Mechanismus der LLM schnell zu einem Mischen
|
||||||
|
oder Verwechseln von präsentierten Informationen führen. Besonders kleine Netze
|
||||||
|
(meist bis ~7b) sind hier anfällig.].
|
||||||
|
|
||||||
|
### Zusammenfassung – Ansatz 1: Vektordatenbank (Embeddings)
|
||||||
|
|
||||||
|
| | **Details** |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Vorgehen** | Inhalte aller Markdown-Dateien in semantische Vektoren kodieren (z.B. mit `nomic-embed-text` ([@ollama_chroma_cookbook])) und in einer Vektor-DB speichern. LLM-Anfragen per Similarity Search mit relevantem Kontext anreichern. |
|
||||||
|
| **Stärken** | _Semantische Suche_ (findet thematisch passende Infos, nicht nur exakte Worttreffer) [@supercharging_obsidian_search]. Skaliert auf große Textmengen. Bereits heute mit lokalen LLMs erprobt (z.B. _Smart Connections_ Plugin) [@smart_connections_plugin]. Gut geeignet für Q&A, Textzusammenfassungen und Link-Vorschläge basierend auf Ähnlichkeit. |
|
||||||
|
| **Schwächen** | Komplexeres Setup (Embedding-Model + DB + Pipeline) [@supercharging_obsidian_search]. Hoher Rechenaufwand für Embeddings bei großen Vaults. Kein explizites Modell von Beziehungen/Metadaten – strukturierte Abfragen (z.B. _"zeige alle Personen ohne ORCID"_) nur mit Zusatzlogik. Kontexttreffer können ungenau sein (erfordert ggf. Feinjustierung). |
|
||||||
|
| **Integrations-Optionen** | Lokale LLM-Einbindung möglich (z.B. LLaMA 2 über Ollama-API). Tools: **ChromaDB**, **Weaviate** oder **FAISS** als Vektorstore; **LangChain/LlamaIndex** für Pipeline-Management. Obsidian-Plugins: _Smart Connections_ (komplett integriert mit lokalem Embedder+LLM) [@smart_connections_plugin]; _Khoj_ (separater Suchassistent mit Embeddings) [@khoj_plugin]. |
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
%%| column: screen-inset-right
|
||||||
|
graph LR
|
||||||
|
A[Obsidian Vault] --> B[Chunking]
|
||||||
|
B --> C[Embedding Model]
|
||||||
|
C --> D[(Vector Database)]
|
||||||
|
E[User Query] --> F[Query Embedding]
|
||||||
|
F --> G[Similarity Search]
|
||||||
|
D --> G
|
||||||
|
G --> H[Relevant Chunks]
|
||||||
|
H --> I[LLM]
|
||||||
|
E --> I
|
||||||
|
I --> J[Response]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Knowledge-Graph-Ansatz: Strukturierte Graphdatenbank
|
||||||
|
|
||||||
|
### Prinzip
|
||||||
|
|
||||||
|
Statt (oder zusätzlich zu) freiem Text wird der Informationsgehalt des Vaults
|
||||||
|
als **Graph** modelliert. Jede Notiz entspricht einem **Knoten** im Graphen (mit
|
||||||
|
Typ-Label gemäß FileClass, z.B. `Person`, `Projekt` etc.). Relationen zwischen
|
||||||
|
Notizen – implizit durch Obsidian-Wikilinks gegeben – werden zu expliziten
|
||||||
|
**Kanten** im Graph (z.B. eine Person "arbeitet in" Organisation, ein Projekt
|
||||||
|
"liefert" ein Deliverable). Auch Metadaten aus YAML können als Knoten oder
|
||||||
|
Properties modelliert werden (z.B. ORCID als Attribut eines Person-Knotens, Tags
|
||||||
|
als Relationen _"hat Schlagwort"_ usw.). Das Ergebnis ist ein **Wissensgraph**,
|
||||||
|
der ähnlich einem klassischen RDF-Triple-Store oder Neo4j-Property-Graph
|
||||||
|
komplexe Abfragen und Analysen ermöglicht.
|
||||||
|
|
||||||
|
### Erstellung des Graphen
|
||||||
|
|
||||||
|
Eine Möglichkeit ist die Obsidian-Daten nach RDF zu exportieren. So beschreibt
|
||||||
|
Pavlyshyn (2023) ein Verfahren, einen Vault ins RDF-Format zu überführen, um
|
||||||
|
"komplexe Abfragen mit klassischen Semantic-Tools" zu ermöglichen
|
||||||
|
[@export_obsidian_to_rdf]. Alternativ kann man direkt in einer Graphdatenbank
|
||||||
|
wie **Neo4j** modellieren. Ein Community-Plugin (_obsidian-neo4j-stream_) hat
|
||||||
|
beispielsweise versucht, den Obsidian-Linkgraph in Neo4j importierbar zu machen
|
||||||
|
[@export_to_common_graph_formats]. Konkret würde man pro Markdown-Datei einen
|
||||||
|
Node mit dessen YAML-Feldern als Properties anlegen. Bestehende Wiki-Links
|
||||||
|
zwischen Dateien werden als ungerichtete oder gerichtete Edges abgebildet (hier
|
||||||
|
kann man, sofern man mehr Semantik will, Link-Typen einführen – z.B. im Text
|
||||||
|
`[[Albert Einstein|Autor]]` könnte der Alias "Autor" als Kanten-Label genutzt
|
||||||
|
werden). Da Obsidian standardmäßig keine typisierten Kanten unterstützt, bleiben
|
||||||
|
Relationstypen begrenzt – Plugins wie _Juggl_ oder _Graph-Link-Types_ erlauben
|
||||||
|
allerdings das Hinzufügen von Link-Metadaten, was für eine genauere
|
||||||
|
Graph-Modellierung hilfreich sein könnte
|
||||||
|
[@personal_knowledge_graphs_in_obsidian]. YAML-Inhalte, die auf andere Notizen
|
||||||
|
referenzieren, können ebenfalls als Kanten kodiert werden (Beispiel: In einer
|
||||||
|
Projekt-Notiz listet das YAML-Feld `team:` mehrere Personen – diese Verweise
|
||||||
|
werden im Graph als Kanten _Projekt —hatTeam→ Person_ umgesetzt). Nicht
|
||||||
|
referenzielle Metadaten (etwa ein ORCID-Wert) bleiben einfach als Datenfeld am
|
||||||
|
Knoten.
|
||||||
|
|
||||||
|
### Nutzung für LLM-Aufgaben
|
||||||
|
|
||||||
|
Ein solcher Graph erlaubt **strukturierte Abfragen** und **Schlussfolgerungen**.
|
||||||
|
Für wiederkehrende Aufgaben kann man den Graph gezielt auswerten. Beispielsweise
|
||||||
|
ließen sich _"alle Personen ohne ORCID"_ mittels einer einfachen Graph-Query
|
||||||
|
ermitteln. Das LLM könnte diese Liste als Input erhalten und dann (ggf. mittels
|
||||||
|
Tools oder Wissensbasis) die fehlenden IDs ergänzen. Auch _Link-Vorschläge_
|
||||||
|
können aus dem Graph gezogen werden: Durch Graph-Analysen wie das Finden von
|
||||||
|
gemeinsamen Nachbarn oder kürzesten Pfaden entdeckt man Verbindungen, die im
|
||||||
|
Vault noch nicht als direkte Links existieren. So könnte man z.B. feststellen,
|
||||||
|
dass zwei Personen an vielen gleichen Meetings teilgenommen
|
||||||
|
haben und dem Nutzer vorschlagen, diese Personen direkt miteinander zu
|
||||||
|
verknüpfen. Oder man erkennt durch _link prediction_ Algorithmen neue mögliche
|
||||||
|
Beziehungen. Forschung und Community sehen hier großes Potential: Eine
|
||||||
|
AI-gestützte Graphanalyse kann helfen, verborgene Zusammenhänge im eigenen
|
||||||
|
Zettelkasten zu finden [@ai_empowered_zettelkasten_with_ner_and_graph_llm]. Mit
|
||||||
|
Graph-basiertem Reasoning ließe sich sogar **neues Wissen entdecken** oder
|
||||||
|
logisch konsistente Antworten generieren
|
||||||
|
[@ai_empowered_zettelkasten_with_ner_and_graph_llm,
|
||||||
|
@personal_knowledge_graphs_in_obsidian] – etwas, das rein embeddings-basierte
|
||||||
|
Ansätze so nicht leisten.
|
||||||
|
|
||||||
|
### Integration mit LLMs
|
||||||
|
|
||||||
|
Die Integration eines Graphen erfordert meist eine **Zwischenschicht**. Ein LLM
|
||||||
|
kann nicht direkt "in" einer Neo4j-Datenbank suchen, aber man kann ihm eine
|
||||||
|
Schnittstelle anbieten. Zwei Strategien sind denkbar:
|
||||||
|
|
||||||
|
1. **Verbalize & Prompt:** Informationen aus dem Graph gezielt ins Prompt
|
||||||
|
einbetten. Z.B. könnte man bei einer Frage wie "In welcher Organisation
|
||||||
|
arbeitet Alice?" erst eine Graphdatenbank-Anfrage (z.B. in Cypher oder
|
||||||
|
SPARQL) ausführen und das Ergebnis (etwa: "Alice – arbeitetBei → AcmeCorp")
|
||||||
|
in Textform dem Modell vorgeben, bevor es antwortet. Solche Abfragen könnte
|
||||||
|
ein LLM theoretisch sogar selbst generieren (LangChain bietet z.B. Agents,
|
||||||
|
die Cypher-Queries formulieren und ausführen können). Für definierte
|
||||||
|
Use-Cases kann man aber auch feste Query-Vorlagen
|
||||||
|
verwenden.
|
||||||
|
1. **LLM-in-the-Loop Graph Reasoning:** Neuere Libraries wie LlamaIndex
|
||||||
|
ermöglichen es, LLMs als Reasoner über Graphen einzusetzen. Der Graph wird
|
||||||
|
dabei intern z.B. als Tripel-Liste gehalten, und das LLM kann mittels
|
||||||
|
promptbasierter Logik Kettenschlüsse durchführen. Allerdings muss der Graph
|
||||||
|
dafür in das Prompt passen (bei sehr vielen Knoten unrealistisch) – es ist
|
||||||
|
also eher für Teilgraphen oder summarische Beziehungen geeignet^[Via 'Tool
|
||||||
|
Use' in Modernen LLM könnte das LLM selbst eine Suche auslösen und so den
|
||||||
|
Teilgraphen wählen. Aber alleine die formulierung der Suche führt dann direkt
|
||||||
|
zu dem hybriden Ansatz unten.].
|
||||||
|
|
||||||
|
Eine andere interessante Möglichkeit ist die Nutzung **graphbasierter
|
||||||
|
KI-Modelle** (Graph Neural Networks o.ä.), die aber in unserem Kontext
|
||||||
|
(persönlicher Vault) noch experimentell sind. Erwähnenswert ist z.B. MyKin.ai,
|
||||||
|
ein Projekt, das einen privaten KI-Assistenten baut, der gemeinsam mit dem
|
||||||
|
Nutzer einen persönlichen Wissensgraphen aufbaut und nutzt
|
||||||
|
[@personal_knowledge_graphs_in_obsidian]. Hier übernimmt die KI das "heavy
|
||||||
|
lifting" der Graph-Pflege, während der Nutzer chattet – ein hybrider Ansatz aus
|
||||||
|
Conversation und Graphaufbau. Für unseren Anwendungsfall wäre jedoch eher ein
|
||||||
|
statischer Graph sinnvoll, den wir periodisch aktualisieren.
|
||||||
|
|
||||||
|
### Zusammenfassung - Ansatz 2: Graphdatenbank
|
||||||
|
|
||||||
|
| | **Details** |
|
||||||
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Vorgehen** | Konvertiere Vault-Inhalte in einen strukturierten Graphen (Knoten = Notizen/Entitäten; Kanten = Obsidian-Links oder abgeleitete Relationen). Nutzen von Graph-DB (Neo4j, RDF-Store) für Abfragen und Analysen. |
|
||||||
|
| **Stärken** | Explizite **Struktur**: ermöglicht genaue Abfragen, z.B. Finde fehlende Werte oder alle Verknüpfungen eines Knotens auf einen Blick. **Logische Inferenzen** möglich (Graph Reasoning) – unterstützt Link-Empfehlungen und Konsistenzprüfungen [@ai_empowered_zettelkasten_with_ner_and_graph_llm, @personal_knowledge_graphs_in_obsidian]. Gute Ergänzung zu YAML-Typisierung: FileClass-Struktur wird vollständig nutzbar. Persistenz: Graph kann unabhängig von Obsidian analysiert, versioniert, mit anderen Daten gemappt werden (z.B. ORCID-Abgleich via externen Datensatz). |
|
||||||
|
| **Schwächen** | Erheblicher **Initialaufwand**: Datenmodell entwerfen, Export-Skripte schreiben oder Tools einrichten [@export_to_common_graph_formats]. Keine fertige Out-of-the-box-Lösung für Obsidian↔Graph (bislang nur Ansätze in der Community). Laufende Synchronisation nötig (Vault-Änderungen -> Graph-Update). Die LLM-Integration ist komplexer – erfordert Query-Tool oder das Einbetten von Graph-Daten ins Prompt. Für offene Fragen (Freitext) allein nicht ausreichend, da der Graph primär Fakten repräsentiert, nicht Fließtext. |
|
||||||
|
| **Integrations-Optionen** | **Neo4j** (mit APOC/Neosemantics für RDF-Unterstützung) eignet sich für Property-Graph-Modell; **Apache Jena** oder GraphDB für RDF-Triple-Store. _LangChain_ bietet Memory/Agent, um Wissensgraphen abzufragen (z.B. ConversationKGMemory). _LlamaIndex_ hat einen KnowledgeGraphIndex, der Tripel extrahieren und durchs LLM traversieren kann. Diese Lösungen sind aber noch experimentell. Evtl. Kombination mit Obsidian-Plugin: Ein früher Plugin-Prototyp streamte Obsidian-Daten nach Neo4j [@export_to_common_graph_formats] – dieser könnte als Ausgangspunkt dienen. |
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
%%| column: screen-inset-right
|
||||||
|
graph LR
|
||||||
|
A[Obsidian Vault] --> B[Entity Extraction]
|
||||||
|
A --> C[Relationship Extraction]
|
||||||
|
B --> D[Graph Construction]
|
||||||
|
C --> D
|
||||||
|
D --> E[(Graph Database)]
|
||||||
|
F[User Query] --> G[Query Parser]
|
||||||
|
G --> H[Graph Traversal]
|
||||||
|
E --> H
|
||||||
|
H --> I[Structured Facts]
|
||||||
|
I --> J[LLM]
|
||||||
|
F --> J
|
||||||
|
J --> K[Response]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fazit Graphdatenbank
|
||||||
|
|
||||||
|
Ein Wissensgraph spielt seine Stärken vor allem bei **strukturbezogenen
|
||||||
|
Aufgaben** aus. Für das automatische Ausfüllen von YAML-Feldern oder das Prüfen
|
||||||
|
von Verlinkungen ist er ideal, da solche Fragen direkte Graphabfragen
|
||||||
|
ermöglichen. Auch für neuartige Verknüpfungen (Link-Vorschläge) lässt sich ein
|
||||||
|
Graph analytisch nutzen (z.B. "Link Prediction" auf Basis von
|
||||||
|
Graph-Nachbarschaft). Allerdings ist die Umsetzung deutlich komplexer als beim
|
||||||
|
Vektorstore, und viele RAG-Anwendungsfälle (Zusammenfassungen, inhaltliche Q&A)
|
||||||
|
erfordern trotzdem den Rückgriff auf die eigentlichen Texte – was wiederum den
|
||||||
|
Vektoransatz benötigt. Daher bietet sich oft eine Kombination beider Methoden
|
||||||
|
an.
|
||||||
|
|
||||||
|
## 3. Hybrid-Ansatz: Kombination aus Graph und Vektor-RAG
|
||||||
|
|
||||||
|
Dieser Ansatz versucht, **semantische Textsuche** und **strukturierte
|
||||||
|
Graph-Abfragen** zu vereinen, um die Vorteile beider Welten auszuschöpfen. In
|
||||||
|
der Praxis gibt es mehrere Möglichkeiten, wie ein hybrides System ausgestaltet
|
||||||
|
sein kann:
|
||||||
|
|
||||||
|
- **Parallelbetrieb mit separaten Pipelines:** Vektorstore und Knowledge Graph
|
||||||
|
werden beide gepflegt. Je nach Anfrage oder Teilaufgabe wird das eine oder
|
||||||
|
andere genutzt. Beispiel: Für eine Q&A-Frage holt das System erst relevante
|
||||||
|
Text-Passagen via Vektorstore, **und** prüft zusätzlich im Graph, welche
|
||||||
|
Entitäten darin vorkommen und ruft deren Beziehungen ab. Das LLM bekäme dann
|
||||||
|
sowohl inhaltliche Ausschnitte als Kontext als auch strukturierte Fakten (z.B.
|
||||||
|
_"Alice arbeitetBei AcmeCorp"_) als Knowledge-Panel. Für die Aufgabe
|
||||||
|
_Link-Vorschläge_ könnte das System sowohl einen Embedding-Vergleich zwischen
|
||||||
|
Notizen nutzen (um thematisch ähnliche Notes zu finden), als auch den Graphen
|
||||||
|
auswerten (um strukturell nahe, aber unverbundene Knoten zu entdecken). Die
|
||||||
|
finalen Vorschläge wären die **Schnittmenge** bzw. **Union** beider Methoden –
|
||||||
|
das erhöht Präzision und Reichweite der Empfehlungen.
|
||||||
|
- **Integration innerhalb einer Datenplattform:** Moderne Vector-Datenbanken wie
|
||||||
|
_Weaviate_ erlauben es, semantische Vektorsuche mit symbolischen Filtern zu
|
||||||
|
kombinieren. Man kann Objekte (hier: Notizen) mit ihren strukturierten Feldern
|
||||||
|
in Weaviate speichern und neben dem Vektorindex auch die Metadaten abfragen.
|
||||||
|
Z.B. könnte man eine Query formulieren: _"Gib mir die 5 ähnlichsten Notizen zu
|
||||||
|
`[Text]`, die vom Typ Projekt sind und nach 2020 erstellt wurden."_ – Weaviate
|
||||||
|
würde erst nach Ähnlichkeit filtern, dann die Metadaten-Bedingungen anwenden.
|
||||||
|
So eine **hybride Suche** könnte man nutzen, um etwa bei Template-Befüllung
|
||||||
|
**nur vergleichbare Objekte** zum Prompt hinzuzufügen (z.B. nur andere
|
||||||
|
Organisationen, keine Meeting-Notizen). Auch ChromaDB arbeitet an
|
||||||
|
Feature-Filterfunktionen, die so etwas erlauben würden. Alternativ kann man
|
||||||
|
den Graphen selbst mit Vektor-Embeddings anreichern: Man könnte jedem
|
||||||
|
Knotentyp einen eigenen Vektor zuordnen, der den gesamten Inhalt der
|
||||||
|
zugehörigen Notiz(en) repräsentiert. Diese Vektoren ließen sich im Graphen als
|
||||||
|
Attribut halten und für Ähnlichkeitssuchen zwischen Knoten verwenden
|
||||||
|
(_knowledge graph embeddings_). Allerdings ist das experimentell – man müsste
|
||||||
|
z.B. bei Kanten-Traversierung dynamisch Nachbarschaftsvektoren kombinieren,
|
||||||
|
was nicht trivial ist.
|
||||||
|
- **LLM als Orchestrator:** Hier steuert das LLM, wann welcher Ansatz gezogen
|
||||||
|
wird. Beispielsweise könnte man ein System bauen, in dem das LLM zunächst
|
||||||
|
entscheidet: _"Brauche ich strukturiertes Wissen?"_ Wenn ja, könnte es per
|
||||||
|
Tool-Use einen Graph-Query durchführen (z.B. via Cypher) – eine Technik, die
|
||||||
|
mit LangChain Agents umsetzbar wäre. Danach würde es ggf. einen zweiten
|
||||||
|
Schritt machen: _"Benötige ich noch Detailinformationen oder Zitate?"_ – dann
|
||||||
|
die Vektor-Datenbank abfragen, relevante Textstücke holen, und schließlich
|
||||||
|
alles in einer konsolidierten Antwort formulieren. Dieser agentenbasierte
|
||||||
|
Ansatz ist sehr flexibel, aber auch am anspruchsvollsten in der
|
||||||
|
Implementierung (er erfordert zuverlässig trainierte/verfeinerte LLM-Prompts,
|
||||||
|
die wissen, wann und wie die jeweiligen Werkzeuge zu benutzen sind).
|
||||||
|
|
||||||
|
### Vor-/Nachteile
|
||||||
|
|
||||||
|
Die Hybridlösung verspricht **maximale Abdeckung** der Anwendungsfälle.
|
||||||
|
Strukturierte Fakten und unstrukturierte Inhalte können gemeinsam dem LLM
|
||||||
|
präsentiert werden, was sowohl präzise Faktenkenntnis als auch reichhaltigen
|
||||||
|
Kontext ermöglicht. Gerade für komplexe Aufgaben – etwa das automatisierte
|
||||||
|
Erstellen einer neuen Entitätenseite – wären wohl beide Aspekte wichtig: das LLM
|
||||||
|
müsste sich an vorhandenen ähnlichen Seiten **inhaltlich** orientieren
|
||||||
|
(Vektorsuche nach ähnlichen Organisations-Beschreibungen) und zugleich
|
||||||
|
**korrekte Verknüpfungen** setzen (Graph checken, ob z.B. die neue Organisation
|
||||||
|
bereits Personen im Vault hat, die als Mitarbeiter verknüpft werden sollten).
|
||||||
|
Ein solches System könnte also dem Nutzer sehr viel Arbeit abnehmen und dabei
|
||||||
|
konsistente, vernetzte Notizen erzeugen.
|
||||||
|
|
||||||
|
Dem steht jedoch ein hoher **Architekturund Wartungsaufwand** gegenüber. Man
|
||||||
|
muss im Grunde zwei Systeme aufbauen und aktuell halten. Zudem ist die Logik,
|
||||||
|
wie die Ergebnisse zusammenfließen, nicht trivial. Ohne gutes Design kann es
|
||||||
|
passieren, dass der Graph-Teil und der Vektor-Teil widersprüchliche oder
|
||||||
|
redundante Informationen liefern. Auch muss man Performance beachten – doppelte
|
||||||
|
Abfragen kosten mehr Zeit. In vielen Fällen mag auch ein einzelner Ansatz
|
||||||
|
ausreichen, sodass die Zusatzkomplexität nicht immer gerechtfertigt ist.
|
||||||
|
|
||||||
|
### Integrationsmöglichkeiten
|
||||||
|
|
||||||
|
Auf technischer Seite ist so ein hybrides System durchaus machbar.
|
||||||
|
Beispielsweise ließe sich **LlamaIndex** verwenden, um unterschiedliche Indexe
|
||||||
|
(VectorIndex, KnowledgeGraphIndex) zu kombinieren – es gibt Konzepte wie
|
||||||
|
"Composable Indices", mit denen man hierarchische Abfragen bauen kann. So könnte
|
||||||
|
man erst den Graph nach relevanten Knoten filtern und dann nur die zugehörigen
|
||||||
|
Dokumente vektor-suchen (oder umgekehrt). Weaviate als All-in-one-Lösung wurde
|
||||||
|
bereits erwähnt. In kleineren Umgebungen kann man auch pragmatisch vorgehen: Ein
|
||||||
|
Python-Skript, das bei bestimmten Fragen zuerst einen Neo4j-Query absetzt und
|
||||||
|
dessen Ergebnis dem LLM als Teil des Prompts voranstellt, während es parallel
|
||||||
|
eine Chroma-Query macht, wäre eine einfache implementierbare Variante.
|
||||||
|
|
||||||
|
### Zusammenfassung – Ansatz 3: Hybrid-Lösung
|
||||||
|
|
||||||
|
| | **Details** |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Vorgehen** | Kombination beider Ansätze: Pflege einer Graph-Struktur **und** eines Vektorindex. Nutzung je nach Bedarf – entweder separat oder durch orchestrierte Abfragen, um sowohl strukturiertes Wissen als auch relevante Texte bereitzustellen. |
|
||||||
|
| **Stärken** | Sehr **leistungsfähig** – deckt sowohl faktische als auch kontextuelle Fragen ab. Kann die **höchste Antwortqualität** liefern (Konsistenz durch Graph-Fakten, Detail durch Textauszüge). Hilft, sowohl "Known-item" Suchen (explizite Werte) als auch "Open-ended" Suchen (Texte) zu bedienen. Für Link-Vorschläge ideal: Kombination aus semantischer Ähnlichkeit und Graph-Nachbarschaft erhöht Trefferquote sinnvoll. |
|
||||||
|
| **Schwächen** | **Sehr komplex** in Umsetzung und Wartung. Erfordert doppelte Infrastruktur. Koordination zwischen Graph und Vectorstore nötig – potenziell fehleranfällig. Höhere Latenz durch Mehrfach-Abfragen. Nur lohnend, wenn wirklich vielfältige Aufgaben automatisiert werden sollen; für rein textliche Q&A overkill. |
|
||||||
|
| **Integrations-Optionen** | Weaviate (Vectors + strukturierte Class-Properties in einem System), oder Kombination aus Neo4j + Chroma. LangChain Agents könnten Graphund Vektor-Tools parallel nutzen. **LlamaIndex** bietet experimentell kombinierbare Indizes. Workflows müssen sorgfältig entworfen werden (z.B. zuerst Graph-Query, dann Vector-Query auf Untermenge). |
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
%%| column: screen-inset-right
|
||||||
|
graph LR
|
||||||
|
A[Obsidian Vault] --> B[Chunking]
|
||||||
|
A --> C[Entity Extraction]
|
||||||
|
B --> D[Embedding Model]
|
||||||
|
C --> E[Graph Construction]
|
||||||
|
D --> F[(Vector Database)]
|
||||||
|
E --> G[(Graph Database)]
|
||||||
|
H[User Query] --> I[Query Embedding]
|
||||||
|
H --> J[Graph Query]
|
||||||
|
I --> K[Vector Search]
|
||||||
|
J --> L[Graph Traversal]
|
||||||
|
F --> K
|
||||||
|
G --> L
|
||||||
|
K --> M[Text Context]
|
||||||
|
L --> N[Structured Facts]
|
||||||
|
M --> O[Combined Context]
|
||||||
|
N --> O
|
||||||
|
H --> P[LLM]
|
||||||
|
O --> P
|
||||||
|
P --> Q[Enriched Response]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fazit Hybrid-Lösung
|
||||||
|
|
||||||
|
Die Hybrid-Lösung ist die **ambitionierteste**, aber auch zukunftsträchtigste
|
||||||
|
Option. Sie empfiehlt sich, wenn sowohl inhaltliche Assistenz (Texte
|
||||||
|
zusammenfassen, beantworten) als auch datenbankartige Operationen (Felder
|
||||||
|
validieren, Beziehungen auswerten) gefragt sind – was hier der Fall ist. Oft
|
||||||
|
kann man auch schrittweise vorgehen: zunächst mit einem Vektor-RAG starten
|
||||||
|
(geringerer Aufwand) und dann gezielt Graph-Features ergänzen, sobald z.B.
|
||||||
|
Link-Empfehlungen oder Konsistenzprüfungen wichtiger werden.
|
||||||
|
|
||||||
|
## 4. Datenaufbereitung: YAML-Metadaten extrahieren und normalisieren
|
||||||
|
|
||||||
|
Unabhängig vom gewählten Retrieval-Ansatz ist es essenziell, die in YAML front
|
||||||
|
matter steckenden strukturierten Informationen effektiv zu nutzen. Die
|
||||||
|
Obsidian-Plugins _Metadata Menu_ und _Templater_ stellen sicher, dass viele
|
||||||
|
wichtige Daten bereits sauber in den Notizen vorliegen (z.B. hat eine
|
||||||
|
Personenseite Felder wie `fullname`, `birthdate`, `ORCID` usw.). Ein LLM könnte
|
||||||
|
zwar theoretisch auch direkt im Markdown nach diesen Mustern suchen, aber es ist
|
||||||
|
deutlich effizienter, die Daten einmalig zu **extrahieren** und in einer
|
||||||
|
leichter nutzbaren Form vorzuhalten.
|
||||||
|
|
||||||
|
### Extraktion
|
||||||
|
|
||||||
|
Ein möglicher Schritt im täglichen Refresh ist ein Skript, das alle Dateien
|
||||||
|
durchläuft und die YAML-Blöcke parst (z.B. mit einem YAML-Parser in Python oder
|
||||||
|
JavaScript). Die extrahierten Felder können dann in eine **normale Datenbank**
|
||||||
|
(SQLite/CSV/JSON) oder direkt als Knoten/Properties in den Knowledge Graph
|
||||||
|
überführt werden. Damit erhält man z.B. eine Tabelle aller Personen mit ihren
|
||||||
|
ORCID-IDs, eine Liste aller Projekte mit Start-/Enddatum etc.
|
||||||
|
|
||||||
|
### Normalisierung
|
||||||
|
|
||||||
|
Oft müssen die Rohwerte etwas vereinheitlicht werden. Beispielsweise sollten
|
||||||
|
Datumsangaben ein konsistentes Format haben, Personennamen evtl. in
|
||||||
|
Vor-/Nachname zerlegt, und fehlende Felder explizit als `null` markiert werden.
|
||||||
|
Außerdem kann man hier foreign-key-Bezüge auflösen: Wenn z.B. im YAML einer
|
||||||
|
Publikation `author: "[[Doe, John]]"` steht, könnte das Skript erkennen, dass
|
||||||
|
dies die Person mit UID XYZ ist, und entsprechend in der extrahierten Struktur
|
||||||
|
statt des Link-Codes einen eindeutigen Verweis (auf die Person John Doe)
|
||||||
|
speichern. Diese Normalisierung erleichtert nachfolgende Analysen enorm –
|
||||||
|
insbesondere kann man einfache Regeln ableiten, die dann vom LLM geprüft oder
|
||||||
|
genutzt werden. Zum Beispiel: _"Wenn `person.ORCID` leer ist, schlage vor, ihn
|
||||||
|
zu ergänzen"_ – das kann das LLM dann direkt als Aufforderung bekommen. Oder:
|
||||||
|
_"Beim Erstellen einer neuen Person fülle Felder X,Y nach Vorlage aus"_ – hier
|
||||||
|
weiß man aus der YAML-Definition bereits, welche Felder existieren müssen.
|
||||||
|
|
||||||
|
### Nutzung durch LLM
|
||||||
|
|
||||||
|
Der aufbereitete YAML-Datensatz kann auf zwei Weisen eingebunden werden:
|
||||||
|
|
||||||
|
- **Inline im Prompt:** Für bestimmte Aufgaben kann man dem LLM direkt
|
||||||
|
Ausschnitte aus dieser strukturierten Sammlung geben. Etwa: _"In unserer
|
||||||
|
Datenbank fehlt für `Person[42]` der ORCID. Hier ist eine Liste aller
|
||||||
|
Personennamen mit ORCID, finde anhand des Namens den passenden ORCID und trage
|
||||||
|
ihn ein."_ – Falls die Person woanders erwähnt wurde, könnte das Modell es
|
||||||
|
herausfinden. (Eher unwahrscheinlich ohne Internetzugriff – ORCID erfordert
|
||||||
|
eher einen API-Call, aber zumindest könnte das LLM erkennen, _dass_ es fehlt
|
||||||
|
und ggf. den Nutzer nach der ID fragen). Für Link-Empfehlungen könnte man dem
|
||||||
|
LLM eine Liste aller Titel geben – oder besser direkt die Graph-Info wie
|
||||||
|
_"Person A und Person B haben 3 gemeinsame Projekte"_ – siehe Hybrid-Ansatz.
|
||||||
|
- **Programmatisch außerhalb des LLMs:** Viele Routineaufgaben lassen sich
|
||||||
|
erkennen, ohne das LLM zu bemühen. Man könnte einen Teil der Automatisierung
|
||||||
|
rein mit Skripten vorab erledigen. Z.B. neue Links: Ein Skript könnte alle
|
||||||
|
Personennamen im Fließtext durchsuchen und prüfen, ob sie bereits als
|
||||||
|
`[[Link]]` markiert sind; wenn nicht, die Stelle hervorheben und dem LLM als
|
||||||
|
_"Kandidat für Verlinkung"_ präsentieren. Oder bei einer neuen Organisation
|
||||||
|
könnten automatisch Felder aus externen APIs gezogen und ins Template
|
||||||
|
eingetragen werden (sofern erlaubt). Das LLM hätte dann eher die Rolle, die
|
||||||
|
zusammengestellten Infos in schönen Prosa-Text zu gießen, anstatt die Fakten
|
||||||
|
selbst zu suchen.
|
||||||
|
|
||||||
|
### Beispiel-Workflows
|
||||||
|
|
||||||
|
- YAML-Exports lassen sich mit vorhandenen Tools unterstützen. Es gibt z.B. das
|
||||||
|
Obsidian-Plugin _Dataview_, welches Abfragen auf YAML ermöglichen kann –
|
||||||
|
allerdings nur innerhalb Obsidian. Man könnte aber ein Dataview
|
||||||
|
JS-Skript^[oder Plugins wie _Dataview-Publisher_ benutzen, die die Ergebnisse
|
||||||
|
als Markdown-Tabell in ein Dokument schreiben] schreiben, das alle Einträge
|
||||||
|
eines Typs ausgibt, und diese Output-Datei dann weiterverarbeiten. Alternativ
|
||||||
|
direkt auf Dateisystemebene arbeiten: Python mit `os` und `pyyaml` kann alle
|
||||||
|
`.md` Files scannen.
|
||||||
|
- Die extrahierten Daten kann man mit dem Graph-Ansatz koppeln: etwa alle
|
||||||
|
Personen ohne ORCID als Cypher-Query generieren lassen und automatisch in eine
|
||||||
|
"ToDo"-Liste (Obsidian Note) schreiben, die vom LLM oder Nutzer geprüft wird.
|
||||||
|
- Durch Templates sind die Felder pro FileClass ja bekannt. Diese Knowledge kann
|
||||||
|
ins Prompt fließen: _"Eine Organisation hat die Felder Name, Typ,
|
||||||
|
Beschreibung, Mitarbeiter, etc. Fülle basierend auf den folgenden Infos…"_ Das
|
||||||
|
Modell weiß dann genau, welche YAML-Spalten es ausgeben soll.
|
||||||
|
|
||||||
|
### Vor- & Nachteile
|
||||||
|
|
||||||
|
Die **Vorteile** der strukturierten Extraktion liegen auf der Hand – Performance
|
||||||
|
und Präzision. Man muss nicht jedes Mal den gesamten Markdown-Text durchsuchen,
|
||||||
|
um z.B. den Wert eines bestimmten Feldes zu finden; man hat ihn direkt. Außerdem
|
||||||
|
reduziert es die Abhängigkeit vom LLM für einfache Aufgaben (Daten finden,
|
||||||
|
vergleichen). Für die meisten Menschen ist es auch leichter zu verstehen und zu
|
||||||
|
prüfen, wenn man z.B. eine CSV mit allen ORCIDs hat, als wenn man dem LLM blind
|
||||||
|
glauben muss.
|
||||||
|
Als **Nachteil** kann gesehen werden, dass es zusätzlicher
|
||||||
|
Implementierungsaufwand ist und eine gewisse Duplizierung der Daten (die
|
||||||
|
YAML-Inhalte leben dann in zwei Formen: im Markdown und in der extrahierten
|
||||||
|
Sammlung). Die Synchronisation muss bei Änderungen immer gewährleistet sein
|
||||||
|
(Cronjob). Allerdings ist das, verglichen mit dem Aufwand der LLM-Integration,
|
||||||
|
relativ gering und gut automatisierbar.
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
%%| column: screen-inset-right
|
||||||
|
graph LR
|
||||||
|
A[Obsidian Vault] --> B[FileClass Detection]
|
||||||
|
B --> C[Type-Specific Extraction]
|
||||||
|
C --> D[YAML Parser]
|
||||||
|
D --> E[Data Validation]
|
||||||
|
E --> F[Type Normalization]
|
||||||
|
F --> G[(Typed Collections)]
|
||||||
|
H[Task Request] --> I[Schema Lookup]
|
||||||
|
I --> J[Targeted Data Fetch]
|
||||||
|
G --> J
|
||||||
|
J --> K[Context Assembly]
|
||||||
|
H --> K
|
||||||
|
K --> L[LLM Processing]
|
||||||
|
L --> M[Schema-Aware Output]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zusammenfassung – Ansatz 4: Extraktion
|
||||||
|
|
||||||
|
In jedem Fall sollte man eine Pipeline vorsehen, die die YAML-**Metadaten
|
||||||
|
extrahiert** und in eine strukturierte Form bringt. Diese bildet das Rückgrat
|
||||||
|
für den Knowledge-Graph-Ansatz (ohne diese wären die Knoten nackte Titel ohne
|
||||||
|
Attribute) und ist auch für Vektor-RAG nützlich (z.B. als Filter oder zur
|
||||||
|
post-processing der LLM-Antworten). Insbesondere dank der FileClass-Typisierung
|
||||||
|
im Vault kann man hier sehr **zielgerichtet** vorgehen – etwa nur definierte
|
||||||
|
Entitätstypen verarbeiten. In Community-Diskussionen wurde vorgeschlagen,
|
||||||
|
YAML-Metadaten zu nutzen, um AI-Aufgaben einzuschränken: z.B. NER-Modelle nur
|
||||||
|
auf bestimmten Notizen laufen zu lassen, die laut YAML einen bestimmten Typ
|
||||||
|
haben [@ai_empowered_zettelkasten_with_ner_and_graph_llm]. Solche Optimierungen
|
||||||
|
werden durch saubere strukturelle Aufbereitung erst möglich.
|
||||||
|
|
||||||
|
## 5. Automatisierungstools und Workflows
|
||||||
|
|
||||||
|
Für die Umsetzung der oben beschriebenen Ansätze gibt es bereits einige Tools,
|
||||||
|
Projekte und Best Practices, die man nutzen oder von denen man lernen kann. Hier
|
||||||
|
eine strukturierte Übersicht samt Empfehlungen:
|
||||||
|
|
||||||
|
### Obsidian-Plugins (In-App KI-Features)
|
||||||
|
|
||||||
|
- _Smart Connections:_ Plugin, das innerhalb Obsidian mit lokalen Embeddings
|
||||||
|
arbeitet, um **ähnliche Notizen** zu finden, und einen Chatbot bereitstellt.
|
||||||
|
Es kann ein lokales LLM (oder OpenAI API) einbinden und versorgt es
|
||||||
|
automatisch mit Kontext aus dem Vault [@smart_connections_plugin]. Vorteil:
|
||||||
|
einfache Installation, enge Vault-Integration (Antworten können direkt als
|
||||||
|
Notiz eingefügt werden). Nachteil: begrenzt anpassbar – der Workflow ist
|
||||||
|
vordefiniert (hauptsächlich Q&A Chat). Für den Start aber exzellent, um ein
|
||||||
|
Gefühl für RAG im eigenen Vault zu bekommen.
|
||||||
|
- _Khoj:_ Ein Open-Source Projekt, bestehend aus einem lokalen Backend
|
||||||
|
[@khoj_plugin] und Obsidian-Plugin. Ermöglicht **natürliche Sprachsuche** und
|
||||||
|
Chat über die eigenen Notizen [@khoj_plugin]. Es kann sowohl online-Modelle
|
||||||
|
(GPT-4 etc.) als auch lokale Modelle nutzen
|
||||||
|
[@build_your_second_brain_with_khoj_ai]. Khoj fokussiert auf schnelle
|
||||||
|
semantische Suche; der Chat-Teil ist vor allem QA-orientiert. Als persönlicher
|
||||||
|
Suchassistent ist es sehr interessant – etwa um via Obsidian Command Palette
|
||||||
|
Fragen ans Vault zu stellen. Es ist weniger darauf ausgelegt, automatisch
|
||||||
|
Links zu erzeugen oder YAML zu verändern (dafür wäre wiederum ein LLM mit
|
||||||
|
Schreibrechten nötig).
|
||||||
|
- _Obsidian Copilot / GPT-Assistant:_ Es existieren mehrere Plugins, die GPT-3/4
|
||||||
|
in Obsidian integrieren (teils auch lokal via LLaMA). Diese sind im Prinzip
|
||||||
|
UI-Verbesserungen, um das LLM "im Editor" zu nutzen. Für RAG kann man sie
|
||||||
|
einsetzen, indem man manuell Kontext reinkopiert, aber automatisches Retrieval
|
||||||
|
bieten sie nicht ohne weiteres.
|
||||||
|
- _Obsidian Neo4j Plugin (Experimentell):_ Das erwähnte _obsidian-neo4j-stream_
|
||||||
|
von \@HEmile [@export_to_common_graph_formats] könnte als Ausgangspunkt
|
||||||
|
dienen, falls man die Graph-Route ausprobieren will. Es war dazu gedacht, den
|
||||||
|
Vault als kontinuierlichen Stream in Neo4j zu spiegeln. Leider wurde es nicht
|
||||||
|
fertiggestellt/maintained. Dennoch ließe sich der Code evtl. anpassen, um
|
||||||
|
zumindest einmalig einen Export durchzuführen. Alternativ: Im Obsidian-Forum
|
||||||
|
gibt es auch Beispiele, wie man mit ein paar Skriptzeilen alle Links
|
||||||
|
extrahieren kann. Zusammen mit den YAML-Daten könnte man so einen
|
||||||
|
Basic-Graphen schon bekommen.
|
||||||
|
|
||||||
|
### Externe Anwendungen / Skripte
|
||||||
|
|
||||||
|
- _LlamaIndex (GPT Index):_ Diese Python-Bibliothek ist eine **Schweizer
|
||||||
|
Taschenmesser** für RAG. Man kann Dokumente laden (Markdown wird unterstützt),
|
||||||
|
unterschiedliche Indizes erstellen (Vector, List, KnowledgeGraph etc.) und
|
||||||
|
Abfragen mit LLM orchestrieren. Sie eignet sich, um schnell Prototypen zu
|
||||||
|
bauen. Beispielsweise könnte man einen **KnowledgeGraphIndex** erstellen, der
|
||||||
|
mittels Instruct-LLM Tripel aus den Notizen extrahiert (z.B. "Person X –
|
||||||
|
arbeitet für – Organisation Y"). Anschließend kann man Abfragen in natürlicher
|
||||||
|
Sprache stellen, die vom LLM in Graph-Traversals übersetzt werden. Oder man
|
||||||
|
nutzt den simpleren VectorIndex auf Markdown-Chunks. LlamaIndex kann auch
|
||||||
|
**Komposition**: man könnte pro FileClass einen Index bauen (z.B. alle
|
||||||
|
Personen in einem VectorIndex, alle Projekte in einem anderen) und dann einen
|
||||||
|
übergeordneten Query laufen lassen. Diese Flexibilität ist mächtig – aber es
|
||||||
|
erfordert eben etwas Programmierung. Für einen produktiven Workflow (täglicher
|
||||||
|
Cronjob) müsste man ein eigenes Python-Skript schreiben, das die Indizes
|
||||||
|
aktualisiert.
|
||||||
|
- _LangChain:_ Ein Framework v.a. für komplexere Chains und Agenten. Es liefert
|
||||||
|
Bausteine, um z.B. eine Tool-using Agent zu bauen, die mit einer **Vector DB
|
||||||
|
Suche** und einer **Graph-DB Abfrage** als Tools ausgestattet ist. Damit ließe
|
||||||
|
sich ein Dialogsystem kreieren, das je nach Frage entscheidet, ob es den
|
||||||
|
Neo4j-Graph oder den Chroma-Vektorindex konsultiert. Allerdings setzt dies
|
||||||
|
einiges an Prompt Engineering voraus, damit der Agent zuverlässig
|
||||||
|
funktioniert. Alternativ kann man LangChain auch einfach nutzen, um entweder
|
||||||
|
Vector-search oder Graph-DB-Queries einzeln bequemer zu machen (es gibt z.B.
|
||||||
|
vorgefertigte Neo4j Retriever-Klassen etc.).
|
||||||
|
- _Haystack:_ Das von deepset (evtl. in der Frage mit "Deepseek" gemeint)
|
||||||
|
entwickelte Open-Source-Toolkit **Haystack** ist ebenfalls auf Dokumenten-QA
|
||||||
|
spezialisiert. Es unterstützt das Indexieren von Markdown, verschiedene
|
||||||
|
Vector-Backends und kann auch Knowledge-Graph-Komponenten integrieren. Zudem
|
||||||
|
hat es Pipeline-Knoten zum z.B. Fragenklassifizieren, dass bestimmte Fragen an
|
||||||
|
bestimmte Reader geleitet werden. Für einen produktiven Einsatz mit lokalem UI
|
||||||
|
ggf. eine Option. Allerdings eher heavy-weight und auf QA fokussiert, weniger
|
||||||
|
auf Wissensbasis-Pflege.
|
||||||
|
- _privateGPT / llama.cpp based scripts:_ Für einfache Frage-Antwort-Systeme auf
|
||||||
|
dem eigenen Vault kann man vorhandene Lösungen wie _privateGPT_ oder _GPT4All_
|
||||||
|
(mit UI) verwenden [@second_brain_assistant_with_obsidian]. Diese bringen
|
||||||
|
einen Großteil der Vector+LLM Pipeline schon fertig mit. Sie indexieren Ordner
|
||||||
|
voller Dokumente (auch Markdown) und erlauben dann Queries an ein lokales
|
||||||
|
Modell. Der Anpassungsspielraum (z.B. andere Tasks als reines QA) ist aber
|
||||||
|
gering. Als **Baseline** sind sie nützlich – man könnte damit z.B. testen, wie
|
||||||
|
gut ein LLM mit den eingebetteten Obsidian-Notizen Fragen beantwortet, und
|
||||||
|
daraus Anforderungen ableiten.
|
||||||
|
- _Basic Memory (basicmachines):_ Ein innovativer Ansatz ist hier zu erwähnen:
|
||||||
|
**Basic Memory** speichert AI-Konversationen als Markdown in Obsidian und baut
|
||||||
|
daraus sukzessive einen semantischen Wissensgraph
|
||||||
|
[@basic_memory_ai_conversations_that_build_knowledge]. D.h. wenn man mit dem
|
||||||
|
LLM chatbasiert arbeitet, erstellt das Tool automatisch Notizen und verbindet
|
||||||
|
sie (z.B. werden erkannte Entitäten verlinkt). Es ist quasi das Gegenstück zu
|
||||||
|
unserem Problem – statt einen bestehenden Vault zu nutzen, erzeugt es einen
|
||||||
|
Vault. Dennoch kann man sich dort Konzepte abschauen: z.B. wie strukturierte
|
||||||
|
Notizen aus LLM-Ausgaben generiert werden können, oder wie man
|
||||||
|
_bi-direktional_ arbeitet (User editiert Notiz, KI liest Änderungen beim
|
||||||
|
nächsten Mal). Basic Memory setzt auf lokale Dateien und betont Privatsphäre,
|
||||||
|
was dem hiesigen Anforderungsprofil ähnelt. Für die konkreten Aufgaben
|
||||||
|
(ORCID-Suche, Link-Vorschlag) liefert es zwar keine fertige Lösung, aber die
|
||||||
|
**Idee, KI beim Nutzer Notizen anlegen/ändern zu lassen,** ist hier praktisch
|
||||||
|
umgesetzt.
|
||||||
|
- **Externe APIs / Datenquellen:**
|
||||||
|
Für bestimmte Felder wie ORCID wird ein rein lokales LLM kaum die Werte
|
||||||
|
erraten können, sofern sie nicht schon irgendwo im Vault stehen. Falls
|
||||||
|
Internetzugriff eine Option ist, könnte man ein Plugin oder einen Workflow
|
||||||
|
integrieren, der **ORCID API** Abfragen durchführt (z.B. über den Namen der
|
||||||
|
Person) und die ID zurückliefert. Ein LLM-Agent könnte auch so einen API-Call
|
||||||
|
ausführen (via Tools in LangChain). Alternativ: Alle bekannten ORCID-IDs der
|
||||||
|
eigenen Personen könnte man in einer Datei sammeln; wenn das LLM eine Lücke
|
||||||
|
findet, bittet es den Nutzer um Input. Hier muss man die Limitierungen eines
|
||||||
|
LLM realistisch sehen und ggf. klassische Automatisierung (API-Skripte)
|
||||||
|
kombinieren.
|
||||||
|
|
||||||
|
```{mermaid}
|
||||||
|
%%| column: screen-inset-right
|
||||||
|
graph LR
|
||||||
|
subgraph Obsidian
|
||||||
|
A[Vault] --> B[Plugins]
|
||||||
|
B --> C[Templater]
|
||||||
|
B --> D[Metadata Menu]
|
||||||
|
B --> E[AI Assistant]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph External Processing
|
||||||
|
A --> F[Daily Export]
|
||||||
|
F --> G[Data Processing]
|
||||||
|
G --> H[LLM Analysis]
|
||||||
|
H --> I[Automation Scripts]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Integration
|
||||||
|
I --> J[Change Proposals]
|
||||||
|
J --> K[User Review]
|
||||||
|
K --> L[Accepted Changes]
|
||||||
|
L --> M[Vault Updates]
|
||||||
|
M --> A
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zusammenfassende Empfehlung
|
||||||
|
|
||||||
|
Für einen ersten Prototypen empfiehlt es sich, mit dem **Vektorstore-Ansatz
|
||||||
|
(1)** zu beginnen, da dieser am schnellsten sichtbare Erfolge bringt. Man kann
|
||||||
|
z.B. mit ChromaDB + einem lokalen LLM experimentieren, oder direkt das
|
||||||
|
Smart-Connections-Plugin ausprobieren, um ein Gefühl für semantische Suche im
|
||||||
|
Vault zu bekommen. Die YAML-Daten sollte man von Anfang an **mit-extrahieren
|
||||||
|
(4)**, da sie die Grundlage für weitere Strukturierungsmaßnahmen bilden.
|
||||||
|
Anschließend kann man gezielt **Graph-Features (2)** ergänzen: etwa den
|
||||||
|
exportierten Vault in Neo4j laden und ein paar Abfragen formulieren, um Missing
|
||||||
|
Links oder fehlende Felder aufzuspüren. Mittelfristig dürfte eine **Kombination
|
||||||
|
(3)** notwendig sein, um sowohl Inhalt als auch Struktur abzudecken – dies kann
|
||||||
|
man Schritt für Schritt angehen (z.B. zunächst Vector-RAG für inhaltliche
|
||||||
|
Fragen, und separate Tools/Reports für strukturierte Checks; später dann
|
||||||
|
Integration zu einem einheitlichen KI-Assistenten). Unterstützend sollte man
|
||||||
|
vorhandene **Tools (5)** nutzen, wo möglich – z.B. Khoj für ad-hoc Fragen, oder
|
||||||
|
LlamaIndex für schnelle Implementierung von Prototypen. Generell gilt: lokale
|
||||||
|
LLMs sind inzwischen leistungsfähig genug für solche Aufgaben, wie die genannten
|
||||||
|
Beispiele zeigen (Chat mit Vault über LLaMA etc.). Wichtig ist es, die
|
||||||
|
**Vault-Organisation** konsequent weiterzuführen (FileClasses, Templates), da
|
||||||
|
ein sauber strukturiertes Wissen die Grundlage für jede erfolgreiche RAG-Lösung
|
||||||
|
ist – egal ob Vektor, Graph oder hybrid.
|
||||||
|
|
||||||
|
## Quellen
|
||||||
|
|
||||||
|
Die Analyse basiert auf aktuellen Erkenntnissen aus der Obsidian-Community und
|
||||||
|
KI-Fachwelt, u.a. Erfahrungen mit semantischer Suche
|
||||||
|
[@smart_connections_plugin], Diskussionen zu Knowledge Graphs in PKM
|
||||||
|
[@ai_empowered_zettelkasten_with_ner_and_graph_llm] und Berichten über lokale
|
||||||
|
RAG-Implementierungen [@local_free_rag_with_question_generation,
|
||||||
|
@smart_connections_plugin].
|
||||||
|
|
||||||
|
## Methodik / LLMs als 'Autoren' {.appendix}
|
||||||
|
|
||||||
|
Erstellt wurde der initial draft mittels Websuche und "Deep-Research" von
|
||||||
|
`gpt-4.5 (preview)`. Systematische Überarbeitungen (Extraktion Bibliographie,
|
||||||
|
Überarbeitung Metadaten) mittels `cogito-v0.1` im Editor. Übernahme nach
|
||||||
|
manueller Prüfung. Erstellung der Mermaid-Diagramme mittels `Claude 3.7 Sonnet`.
|
||||||
|
Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting
|
||||||
|
durch Nicole Dresselhaus.
|
3
Writing/_metadata.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
google-scholar: true
|
||||||
|
execute:
|
||||||
|
enable: false
|
69
Writing/documentation.bib
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
@article{wilson2017good,
|
||||||
|
title={Good enough practices in scientific computing},
|
||||||
|
author={Wilson, Greg and Bryan, Jennifer and Cranston, Karen and Kitzes, Justin and Nederbragt, Lex and Teal, Tracy K},
|
||||||
|
journal={PLoS computational biology},
|
||||||
|
volume={13},
|
||||||
|
number={6},
|
||||||
|
pages={e1005510},
|
||||||
|
year={2017},
|
||||||
|
publisher={Public Library of Science}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{prlic2012ten,
|
||||||
|
title={Ten simple rules for documenting scientific software},
|
||||||
|
author={Prli{\'c}, Andreas and Procter, James B},
|
||||||
|
journal={PLoS Computational Biology},
|
||||||
|
volume={8},
|
||||||
|
number={12},
|
||||||
|
pages={e1002802},
|
||||||
|
year={2012},
|
||||||
|
publisher={Public Library of Science}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{smith2016software,
|
||||||
|
title={Software citation principles},
|
||||||
|
author={Smith, Arfon M and Katz, Daniel S and Niemeyer, Kyle E and FORCE11 Software Citation Working Group and others},
|
||||||
|
journal={PeerJ Computer Science},
|
||||||
|
volume={2},
|
||||||
|
pages={e86},
|
||||||
|
year={2016},
|
||||||
|
publisher={PeerJ Inc.}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{maria2019jupyter,
|
||||||
|
title={Jupyter notebooks—a publishing format for reproducible computational workflows},
|
||||||
|
author={Kluyver, Thomas and Ragan-Kelley, Benjamin and P{\'e}rez, Fernando and Granger, Brian and Bussonnier, Matthias and Frederic, Jonathan and Kelley, Kyle and Hamrick, Jessica B and Grout, Jason and Corlay, Sylvain and others},
|
||||||
|
journal={Positioning and Power in Academic Publishing: Players, Agents and Agendas},
|
||||||
|
volume={20},
|
||||||
|
pages={87--90},
|
||||||
|
year={2016},
|
||||||
|
publisher={IOS Press}
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{endings2020principles,
|
||||||
|
title = {Endings Principles for Digital Longevity},
|
||||||
|
author = {{Endings Project}},
|
||||||
|
year = {2020},
|
||||||
|
url = {https://endings.uvic.ca/principles.html}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{katz2021open,
|
||||||
|
title={The Journal of Open Source Software (JOSS)},
|
||||||
|
author={Katz, Daniel S and Niemeyer, Kyle E and Smith, Arfon M},
|
||||||
|
journal={PeerJ Computer Science},
|
||||||
|
volume={7},
|
||||||
|
pages={e432},
|
||||||
|
year={2021},
|
||||||
|
publisher={PeerJ Inc.}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{lamprecht2020towards,
|
||||||
|
title={Towards FAIR principles for research software},
|
||||||
|
author={Lamprecht, Anna-Lena and Garcia, Leyla and Kuzak, Mateusz and Martinez, Carlos and Arcila, Ricardo and Martin Del Pico, Eva and others},
|
||||||
|
journal={Data Science},
|
||||||
|
volume={3},
|
||||||
|
number={1},
|
||||||
|
pages={37--59},
|
||||||
|
year={2020},
|
||||||
|
publisher={IOS Press}
|
||||||
|
}
|
824
Writing/documentation.md
Normal file
@ -0,0 +1,824 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Writing
|
||||||
|
cssclasses:
|
||||||
|
- table-wide
|
||||||
|
- table-wrap
|
||||||
|
title:
|
||||||
|
"Anforderungskatalog für die Dokumentation von Forschungssoftware (Digital
|
||||||
|
Humanities)"
|
||||||
|
description: |
|
||||||
|
Ein Überblick und Best Practices für die Dokumantation von Forschungssoftware.
|
||||||
|
abstract: |
|
||||||
|
Diese Dokumentation fasst zusammen, welche wissenschaftlichen Konzepte,
|
||||||
|
Algorithmen und Theorien hinter der Software stehen. Sie dient dazu, den
|
||||||
|
Nutzer*innen zu helfen, die theoretischen Grundlagen nachvollziehbar zu machen.
|
||||||
|
lang: de
|
||||||
|
authors:
|
||||||
|
- name: Nicole Dresselhaus
|
||||||
|
affiliation:
|
||||||
|
- name: Humboldt-Universität zu Berlin
|
||||||
|
url: https://hu-berlin.de
|
||||||
|
email: nicole.dresselhaus@hu-berlin.de
|
||||||
|
correspondence: true
|
||||||
|
orcid: 0009-0008-8850-3679
|
||||||
|
roles:
|
||||||
|
- Conceptualization
|
||||||
|
- Supervision
|
||||||
|
- Validation
|
||||||
|
- "Writing – review & editing"
|
||||||
|
- name: GPT-4.5
|
||||||
|
url: https://chatgpt.com
|
||||||
|
affiliation:
|
||||||
|
- name: OpenAI
|
||||||
|
url: https://openai.com
|
||||||
|
roles:
|
||||||
|
- investigation
|
||||||
|
- "Writing – original draft"
|
||||||
|
date: 2025-05-08
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- Best Practices
|
||||||
|
citation: true
|
||||||
|
google-scholar: true
|
||||||
|
fileClass: authored
|
||||||
|
bibliography:
|
||||||
|
- documentation.bib
|
||||||
|
image: ../thumbs/writing_documentation.png
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einleitung
|
||||||
|
|
||||||
|
Die **Dokumentation von Forschungssoftware** ist entscheidend, um
|
||||||
|
wissenschaftliche Ergebnisse nachvollziehbar und Software für andere nutzbar zu
|
||||||
|
machen. Insbesondere in den Digital Humanities (etwa in der
|
||||||
|
Geschichtswissenschaft) entwickeln Forschende neben Forschung und Lehre oft
|
||||||
|
eigene Software – meist unter hohem Zeitdruck und ohne formale Ausbildung in
|
||||||
|
Softwareentwicklung. Häufig bleibt die Dokumentation deshalb minimal oder
|
||||||
|
unvollständig, was dazu führt, dass andere (und sogar die Autor\*innen selbst)
|
||||||
|
viel Zeit aufwenden müssen, um den Code zu verstehen und anzuwenden. Dabei gilt
|
||||||
|
gute Dokumentation als zentrale Voraussetzung, um Forschungssoftware
|
||||||
|
**auffindbar, nachvollziehbar und wiederverwendbar** zu machen.
|
||||||
|
|
||||||
|
[Alle Empfehlungen stützen sich auf Literatur und etablierte Richtlinien
|
||||||
|
[@prlic2012ten; @wilson2017good; @katz2021open;
|
||||||
|
@endings2020principles].]{.aside}
|
||||||
|
|
||||||
|
Dieser Anforderungskatalog richtet sich an Forschende, die keine
|
||||||
|
Vollzeit-Programmierer sind, und soll **wissenschaftlich fundierte Richtlinien**
|
||||||
|
für die Dokumentation von Forschungssoftware liefern. Die Empfehlungen
|
||||||
|
berücksichtigen Best Practices des Research Software Engineering (RSE) und
|
||||||
|
insbesondere die Prinzipien des _Endings-Projekts_ für digitale Langlebigkeit
|
||||||
|
[@endings2020principles]. Ziel ist es, ein praxistaugliches Gerüst
|
||||||
|
bereitzustellen, das – trotz Zeitknappheit – die wesentlichen
|
||||||
|
Dokumentationsaspekte abdeckt, um sowohl die **Nachvollziehbarkeit** der
|
||||||
|
Ergebnisse als auch eine **Weiterverwendung** der Software zu ermöglichen. Im
|
||||||
|
Folgenden werden die Anforderungen an Inhalt, Format und Umfang der
|
||||||
|
Dokumentation definiert, geeignete (teil-)automatisierte Dokumentationswerkzeuge
|
||||||
|
diskutiert und Best Practices in Form von Vorlagen und Checklisten vorgestellt.
|
||||||
|
|
||||||
|
## Inhaltliche Anforderungen an die Dokumentation
|
||||||
|
|
||||||
|
Ein zentrales Problem in der Dokumentation wissenschaftlicher Software ist oft
|
||||||
|
das fehlende _Big Picture_, also eine klare Darstellung des _Was_ und _Warum_.
|
||||||
|
Die Dokumentation sollte daher alle **Informationen abdecken, die zum Verstehen,
|
||||||
|
Nutzen und Weiterentwickeln der Software nötig sind**. Insbesondere sind
|
||||||
|
folgende Inhalte essenziell:
|
||||||
|
|
||||||
|
### Ziel und Zweck der Software (Statement of Need)
|
||||||
|
|
||||||
|
Beschreiben Sie _was die Software tut_ und _warum sie entwickelt wurde_. Nennen
|
||||||
|
Sie den wissenschaftlichen Zweck, das Forschungsproblem oder die Fragestellung,
|
||||||
|
die mit der Software adressiert wird, sowie die _Zielgruppe_ (wer soll sie
|
||||||
|
nutzen?). Dieser Kontext hilft anderen, den Nutzen der Software einzuschätzen.
|
||||||
|
Beispiel: _“Dieses Tool extrahiert Personen-Netzwerke aus historischen
|
||||||
|
Briefkorpora, um sozialwissenschaftliche Analysen zu ermöglichen.”_ Eine klare
|
||||||
|
Problem- und Zielbeschreibung richtet sich auch nach dem Umfeld ähnlicher
|
||||||
|
Lösungen – falls es bereits etablierte Tools gibt, sollte die Dokumentation die
|
||||||
|
eigene Herangehensweise einordnen (z. B. was die Software anders oder besser
|
||||||
|
macht).
|
||||||
|
|
||||||
|
### Input-/Output-Spezifikation und Datenbeschreibung
|
||||||
|
|
||||||
|
Dokumentieren Sie alle _Eingabeformate, Ausgabedaten und verwendeten
|
||||||
|
Datensätze_. Nutzer\*innen müssen wissen, welche Daten die Software erwartet
|
||||||
|
(Dateiformate, Schnittstellen, Parameter) und welche Ergebnisse sie produziert.
|
||||||
|
Idealerweise werden Beispiele angegeben: z. B. Beispiel-Dateien oder -Parameter
|
||||||
|
und die korrespondierende Ausgabe. Falls die Software mit bestimmten
|
||||||
|
Forschungsdaten arbeitet, beschreiben Sie diese Daten und ihre Struktur. Dies
|
||||||
|
umfasst die **Datenmodelle** (etwa wichtige Felder, deren Bedeutung und
|
||||||
|
kontrollierte Vokabulare) und Annahmen über die Daten. Gemäß den
|
||||||
|
ENDINGS-Prinzipien sollte die Datenstruktur in einem _statischen Dokument_
|
||||||
|
festgehalten und der Software beigelegt sein – so bleibt nachvollziehbar, wie
|
||||||
|
die Software die Daten interpretiert. Eine Tabelle oder Auflistung der
|
||||||
|
Eingabefelder und Ausgabegrößen mit kurzen Beschreibungen erhöht die Klarheit.
|
||||||
|
[Beispiel: _“Eingabedatei: CSV mit Spalten `Autor`, `Empfänger`, ...; Ausgabe:
|
||||||
|
JSON-Datei mit Netzwerk-Metriken pro Briefwechsel.”_]{.aside}
|
||||||
|
|
||||||
|
### Code-Abhängigkeiten und technische Voraussetzungen
|
||||||
|
|
||||||
|
Listen Sie alle _Abhängigkeiten_ (Dependencies) der Software auf. Dazu gehören
|
||||||
|
verwendete Programmiersprachen/Versionen, erforderliche Bibliotheken oder
|
||||||
|
Frameworks, und sonstige Systemvoraussetzungen (z. B. Betriebssystem,
|
||||||
|
Mindesthardware, Datenbank-Versionen). Wichtig ist, **wie** diese Abhängigkeiten
|
||||||
|
installiert werden können. Optimal ist eine automatisierte Installationsroutine
|
||||||
|
(z. B. ein `requirements.txt` für Python oder ein Paketmanager-Befehl). In jedem
|
||||||
|
Fall sollte die Dokumentation mindestens
|
||||||
|
Schritt-für-Schritt-Installationsanleitungen enthalten (inklusive evtl.
|
||||||
|
benötigter Vorkenntnisse, z. B. _“Python 3 erforderlich”_). [Beispiel:
|
||||||
|
_“Benötigt Python 3.9 und die Bibliotheken Pandas und NetworkX. Installation:
|
||||||
|
`pip install -r requirements.txt`.”_ Falls spezielle technische Voraussetzungen
|
||||||
|
bestehen – etwa Zugriff auf bestimmte Hardware, ein Hochleistungsrechner oder
|
||||||
|
große Speicherkapazitäten – sind diese zu nennen.]{.aside}
|
||||||
|
|
||||||
|
- **Typische Nutzungsszenarien und Workflows:** Zeigen Sie anhand von
|
||||||
|
_Beispielen_, wie die Software benutzt wird. Ein **Quickstart-Beispiel** senkt
|
||||||
|
die Einstiegshürde enorm. Dies kann z. B. eine Anleitung sein, wie man mit
|
||||||
|
wenigen Schritten von einer Eingabedatei zum gewünschten Ergebnis kommt
|
||||||
|
(_“Getting Started”_-Abschnitt). Beschreiben Sie typische Workflows in
|
||||||
|
nachvollziehbaren Schritten: Eingabe vorbereiten, Software-Befehl/GUI-Aktion
|
||||||
|
ausführen, Ausgabe interpretieren. Ggf. können mehrere Anwendungsfälle
|
||||||
|
skizziert werden (z. B. _“Analyse eines einzelnen Briefes”_ vs.
|
||||||
|
_“Batch-Verarbeitung eines gesamten Korpus”_). Diese Beispiele sollten
|
||||||
|
realistisch und möglichst _repräsentativ für wissenschaftliche Anwendungen_
|
||||||
|
sein. Nutzen Sie gerne kleine Datensamples oder Defaults, damit Nutzer die
|
||||||
|
Beispielschritte direkt ausprobieren können. Idealerweise werden
|
||||||
|
Code-Beispiele mit ausgegebenen Resultaten gezeigt (z. B. in Form von
|
||||||
|
Ausschnitten oder, bei Kommandozeilentools, via `--help` dokumentiert).
|
||||||
|
[Faustregel: **Zeigen statt nur beschreiben** – konkrete Anwendungsfälle in
|
||||||
|
der Doku verankern.]{.aside}
|
||||||
|
|
||||||
|
### Wissenschaftlicher Hintergrund und theoretischer Kontext
|
||||||
|
|
||||||
|
Da es sich um Forschungssoftware handelt, sollten Sie den _wissenschaftlichen
|
||||||
|
Kontext_ offenlegen. Das heißt, erklären Sie die grundlegenden Methoden,
|
||||||
|
Algorithmen oder Modelle, die in der Software umgesetzt sind, zumindest in
|
||||||
|
Überblicksform. Verweisen Sie auf _relevante Publikationen_ oder Theorien, damit
|
||||||
|
andere die wissenschaftliche Grundlage nachvollziehen können. Beispielsweise:
|
||||||
|
_“Die Implementierung folgt dem Algorithmus von Müller et al. (2019) zur
|
||||||
|
Netzwerkanalyse historischer Korrespondenz.”_ Halten Sie diesen Abschnitt aber
|
||||||
|
prägnant – Details gehören in die Forschungsarbeit selbst. Wichtig ist, dass die
|
||||||
|
Dokumentation den **Brückenschlag zwischen Code und Forschung** herstellt. Da
|
||||||
|
viele Wissenschaftler\*innen zentrale Aspekte lieber in ihren Artikeln
|
||||||
|
dokumentieren, sollte in der Software-Dokumentation zumindest eine
|
||||||
|
Zusammenfassung mit Querverweis erfolgen. So wissen Nutzer\*innen, unter welchen
|
||||||
|
Annahmen oder Theorien das Tool funktioniert. [Dieser Hintergrundteil
|
||||||
|
unterscheidet Forschungssoftware-Dokumentation von rein kommerzieller
|
||||||
|
Dokumentation: Es geht nicht nur um _wie_ man das Tool benutzt, sondern auch
|
||||||
|
_warum_ es so funktioniert (Stichwort Nachvollziehbarkeit).]{.aside}
|
||||||
|
|
||||||
|
### Bekannte Limitationen, Annahmen und Fehlermeldungen
|
||||||
|
|
||||||
|
Geben Sie ehrlich Auskunft über die _Grenzen der Software_. Welche Fälle werden
|
||||||
|
**nicht** abgedeckt? Welche Annahmen über die Daten oder Anwendungsszenarien
|
||||||
|
werden getroffen? Dokumentieren Sie bekannte Probleme oder Einschränkungen
|
||||||
|
(z. B. _“funktioniert nur für Deutschsprachige Texte”, “maximale Datenmenge 1
|
||||||
|
Mio. Datensätze, da Speicherbegrenzung”_). Solche Hinweise verhindern
|
||||||
|
Fehlanwendungen und sparen Nutzern Zeit. Falls es bekannte **Bugs oder
|
||||||
|
Workarounds** gibt, sollten diese ebenfalls (etwa in einer FAQ oder einem
|
||||||
|
Abschnitt "Bekannte Probleme") erwähnt werden. Eine transparente Auflistung von
|
||||||
|
Limitationen erhöht die Vertrauenswürdigkeit und hilft anderen, die Ergebnisse
|
||||||
|
richtig einzuordnen. Auch **aussagekräftige Fehlermeldungen** im Programm selbst
|
||||||
|
sind eine Form von Dokumentation: Sie sollten nicht nur kryptisch abbrechen,
|
||||||
|
sondern dem/der Anwender\*in idealerweise mitteilen, was schiefging und wie es
|
||||||
|
behoben werden kann (z. B. _“Fehler: Ungültiges Datum im Feld XY – bitte Format
|
||||||
|
TT/MM/JJJJ verwenden.”_). Solche in den Code integrierten Hinweise ergänzen die
|
||||||
|
schriftliche Dokumentation und tragen zur besseren Nutzbarkeit bei.
|
||||||
|
|
||||||
|
### Weiterentwicklung und Beitragsmöglichkeiten
|
||||||
|
|
||||||
|
Obwohl viele Digital-Humanities-Tools primär von Einzelpersonen genutzt werden,
|
||||||
|
sollte dennoch angegeben werden, wie andere ggf. _zur Software beitragen oder
|
||||||
|
Support erhalten_ können. Ein kurzer Hinweis auf den Issue-Tracker (z. B.
|
||||||
|
_“Fehler bitte über GitHub-Issues melden”_) oder auf die Kontaktmöglichkeit zum
|
||||||
|
Autor (E-Mail) gehört dazu. Ebenso können **Community Guidelines** skizziert
|
||||||
|
werden: etwa Codierstandards oder ein Verhaltenskodex, falls Beiträge erwartet
|
||||||
|
werden. Für kleinere Projekte reicht oft ein Satz wie _“Beiträge durch Pull
|
||||||
|
Requests sind willkommen; bei Fragen wenden Sie sich an…”_. [Dieser Aspekt muss
|
||||||
|
nicht umfangreich sein, zeigt aber Offenheit und sorgt dafür, dass im Falle von
|
||||||
|
Rückfragen die Hürde für Kontaktaufnahme niedrig ist.]{.aside}
|
||||||
|
|
||||||
|
### Projekt-Metadaten (Lizenz, Zitation, Version)
|
||||||
|
|
||||||
|
Teil der Dokumentation sind auch formale Informationen, die im Repository leicht
|
||||||
|
zugänglich sein sollten. **Lizenzinformationen** klären die rechtlichen
|
||||||
|
Bedingungen der Nutzung und Weiterverbreitung. Es ist Best Practice, eine
|
||||||
|
**LICENSE-Datei** beizulegen, aber auch in der README kurz zu erwähnen, unter
|
||||||
|
welcher Lizenz die Software steht. Für Forschungssoftware empfiehlt sich eine
|
||||||
|
offene Lizenz (z. B. MIT, BSD oder Apache 2.0 für Code, CC-BY für Daten), um
|
||||||
|
Nachnutzung nicht zu behindern. Zudem sollte angegeben werden, wie die Software
|
||||||
|
**zitiert** werden kann (z. B. DOI, Paper-Referenz). Ein eigener Abschnitt
|
||||||
|
_“Zitation”_ oder eine CITATION-Datei beschreibt, welche Publikation oder
|
||||||
|
welcher DOI bei Verwendung der Software in wissenschaftlichen Arbeiten anzugeben
|
||||||
|
ist. Dies erhöht die akademische Sichtbarkeit und stellt sicher, dass
|
||||||
|
Autor\*innen Credits für ihre Software bekommen[@smith2016software]. Schließlich
|
||||||
|
ist es sinnvoll, eine **Versionsnummer** der Software zu nennen (idealerweise in
|
||||||
|
README und im Tool selbst), damit Nutzer wissen, auf welche Ausgabe sich die
|
||||||
|
Dokumentation bezieht – insbesondere, wenn es im Laufe der Zeit Aktualisierungen
|
||||||
|
gibt. Diese Praxis entspricht auch den ENDINGS-Prinzipien, die verlangen, dass
|
||||||
|
jede veröffentlichte Version eindeutig erkennbar ist und zitiert werden kann.
|
||||||
|
|
||||||
|
### Zusammenfassung der inhaltlichen Anforderungen
|
||||||
|
|
||||||
|
Zusammengefasst sollte die Dokumentation alle **W-Fragen** beantworten: _Was_
|
||||||
|
tut die Software, _warum_ wurde sie geschrieben (wissenschaftlicher Zweck),
|
||||||
|
_wer_ soll sie nutzen, _wie_ wird sie benutzt (Inputs, Outputs, Abläufe),
|
||||||
|
_womit_ läuft sie (Umgebung/Abhängigkeiten), _unter welchen Bedingungen_
|
||||||
|
(Annahmen/Limitationen) und _wohin_ können sich Nutzer wenden
|
||||||
|
(Support/Zitation). All diese Punkte sorgen für **Nachvollziehbarkeit** (im
|
||||||
|
Sinne von Reproduzierbarkeit der Ergebnisse) und **Weiterverwendbarkeit** (im
|
||||||
|
Sinne von Adaptierbarkeit der Software für neue Kontexte).
|
||||||
|
|
||||||
|
## Format und Struktur der Dokumentation
|
||||||
|
|
||||||
|
Für Forschende ohne viel Ressourcen muss die Dokumentation **einfach zugänglich,
|
||||||
|
leicht pflegbar und ohne Spezialsoftware** erstellbar sein. Daher empfiehlt es
|
||||||
|
sich, auf **leichte Formate** und eine klare Struktur zu setzen:
|
||||||
|
|
||||||
|
### `README.md` als zentrales Dokument
|
||||||
|
|
||||||
|
Die Hauptdokumentation sollte als README in Markdown-Format im Hauptverzeichnis
|
||||||
|
des Code-Repositoriums liegen. Dieses README fungiert als “Startseite” des
|
||||||
|
Projekts und enthält idealerweise eine komprimierte Übersicht aller wichtigen
|
||||||
|
Punkte: Zweck der Software, Kurzbeschreibung, Installation, kurzer
|
||||||
|
Nutzungsbeispiel, Kontakt/Lizenz. Auf Plattformen wie GitHub, GitLab etc. wird
|
||||||
|
die README automatisch angezeigt, was die Sichtbarkeit erhöht. Die Vorteile von
|
||||||
|
**Markdown** sind die einfache Lesbarkeit in Rohform, die breite Unterstützung
|
||||||
|
(auch in Renderern wie GitHub-Webansicht) und die Eignung für Versionierung
|
||||||
|
(Textdatei im Git). So bleibt die Dokumentation eng mit dem Code verzahnt und
|
||||||
|
unter Versionskontrolle – ein Prinzip, das auch von ENDINGS propagiert wird
|
||||||
|
(Dokumentation soll statisch und zusammen mit den Daten/Code abgelegt werden).
|
||||||
|
|
||||||
|
### Strukturierte Unterteilung in weitere Dateien/Abschnitte
|
||||||
|
|
||||||
|
::: {.column-margin}
|
||||||
|
|
||||||
|
```plain
|
||||||
|
example-project/
|
||||||
|
├── README.md
|
||||||
|
├── CONTRIBUTING.md (optional)
|
||||||
|
├── CHANGELOG.md (optional)
|
||||||
|
├── CITATION.md (oder CITATION.cff)
|
||||||
|
├── LICENSE
|
||||||
|
├── data/ (optional)
|
||||||
|
│ └── sample_data.csv
|
||||||
|
├── docs/ (optional)
|
||||||
|
│ ├── INSTALL.md
|
||||||
|
│ └── USAGE.md
|
||||||
|
├── examples/ (optional)
|
||||||
|
│ └── example_workflow.ipynb
|
||||||
|
└── src/
|
||||||
|
├── script.py
|
||||||
|
└── module/
|
||||||
|
└── helper.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispielhafter Struktur eines Code-Repositories
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Sollte die Dokumentation umfangreicher sein, ist es sinnvoll, sie in logisch
|
||||||
|
getrennte Abschnitte aufzuteilen. Dies kann innerhalb der README durch
|
||||||
|
Überschriften geschehen oder durch **zusätzliche Markdown-Dateien** im
|
||||||
|
Repository (z. B. eine `INSTALL.md` für ausführliche Installationshinweise, eine
|
||||||
|
`USAGE.md` oder `TUTORIAL.md` für detaillierte Benutzeranleitungen, eine
|
||||||
|
`CHANGELOG.md` für Changelog etc.). Eine gängige Struktur ist z. B.:
|
||||||
|
|
||||||
|
- `README.md` – Überblick (Ziel, Installation, kurzes Beispiel, Lizenz/Zitation)
|
||||||
|
- `docs/` Verzeichnis mit weiteren .md-Dateien für tiefergehende Dokumentation
|
||||||
|
(optional)
|
||||||
|
- `CONTRIBUTING.md` – Hinweise für Beiträger (falls relevant)
|
||||||
|
- `LICENSE` – Lizenztext
|
||||||
|
- `CITATION.cff` oder `CITATION.md` – wie zu zitieren.
|
||||||
|
|
||||||
|
Diese Dateien sollten konsistent formatiert und benannt sein, damit sie leicht
|
||||||
|
auffindbar sind. Sie kommen ohne spezielle Tools aus – ein einfacher Texteditor
|
||||||
|
genügt zum Bearbeiten. Auch **Wiki-Seiten** (etwa in GitHub) können genutzt
|
||||||
|
werden, sind aber weniger dauerhaft versioniert im Vergleich zu Dateien im
|
||||||
|
Code-Repository selbst. Die Dokumentation sollte möglichst _im Repository_
|
||||||
|
selbst liegen, um sicherzustellen, dass sie gemeinsam mit dem Code versioniert,
|
||||||
|
verteilt und archiviert wird. Externe Dokumentationswebsites sind für kleine
|
||||||
|
Projekte oft Overkill und können im schlimmsten Fall verwaisen.
|
||||||
|
|
||||||
|
### Keine proprietären Formate oder Abhängigkeit von Werkzeugen
|
||||||
|
|
||||||
|
Um Hürden für die Erstellung und Nutzung der Dokumentation gering zu halten,
|
||||||
|
sollte auf gängige, offene Formate gesetzt werden (Plaintext, Markdown,
|
||||||
|
reStructuredText). Vermeiden Sie nach Möglichkeit Formate wie Word-Dokumente
|
||||||
|
oder PDF als primäre Dokumentationsquelle – solche Formate sind nicht
|
||||||
|
diff-freundlich, erschweren Zusammenarbeits-Workflows und sind meist nicht Teil
|
||||||
|
des Versionskontrollsystems. Ein Markdown-Dokument hingegen kann gemeinsam mit
|
||||||
|
dem Code gepflegt werden, und Änderungen sind transparent nachvollziehbar. Zudem
|
||||||
|
erlauben offene Formate eine leichtere **Langzeitarchivierung**: Gemäß
|
||||||
|
Endings-Prinzip sollten Informationsressourcen in langfristig lesbaren Formaten
|
||||||
|
vorliegen. Markdown/Plaintext erfüllt diese Bedingung (im Gegensatz etwa zu
|
||||||
|
einer Datenbank-gestützten Wissensbasis oder einem proprietären Wiki, das in 10
|
||||||
|
Jahren evtl. nicht mehr läuft). Im Sinne der _Digital Longevity_ ist eine
|
||||||
|
**statische HTML- oder PDF-Version** der Dokumentation (automatisch generiert
|
||||||
|
aus Markdown) als Teil der Release-Artefakte sinnvoll – so kann z. B. in jeder
|
||||||
|
veröffentlichten Version ein PDF-Handbuch beigelegt werden, das später zitiert
|
||||||
|
oder referenziert werden kann. **Wichtig ist aber, dass die Quelle der Wahrheit
|
||||||
|
immer die im Repository gepflegte Doku bleibt.**
|
||||||
|
|
||||||
|
### Übersichtlichkeit und Navigierbarkeit
|
||||||
|
|
||||||
|
Strukturieren Sie die Dokumentation mit klaren Überschriften und Listen, damit
|
||||||
|
Leser schnell die gesuchten Informationen finden. Eine **logische Gliederung**
|
||||||
|
(wie in diesem Katalog: Einführung, Anforderungen, Installation, Nutzung,
|
||||||
|
Hintergrund, etc.) hilft unterschiedlichen Nutzergruppen gezielt das Relevante
|
||||||
|
zu finden. Für längere Dokumente kann ein Inhaltsverzeichnis oder eine
|
||||||
|
Abschnittsübersicht am Anfang nützlich sein. Markdown bietet z. B. automatische
|
||||||
|
Toc-Generierung auf manchen Plattformen. Achten Sie darauf, pro Abschnitt nur
|
||||||
|
zusammenhängende Informationen zu behandeln (z. B. alles zu Installation an
|
||||||
|
einem Ort). Wiederholungen sollten vermieden werden: lieber an einer Stelle
|
||||||
|
ausführlich dokumentieren und sonst darauf verweisen, um Konsistenzprobleme zu
|
||||||
|
vermeiden (_"Don’t Repeat Yourself"_ gilt auch für Dokumentation). Bei ähnlichen
|
||||||
|
Projekten können Sie sich an bestehenden **Dokumentationsvorlagen** orientieren:
|
||||||
|
Viele erfolgreiche Open-Source-Projekte haben auf GitHub eine ähnliche
|
||||||
|
README-Struktur, die als informelles Template dienen kann.
|
||||||
|
|
||||||
|
### Beispiele, Codeblöcke und ggf. Abbildungen einbinden
|
||||||
|
|
||||||
|
Nutzen Sie die Möglichkeiten von Markdown, um die Dokumentation lebendig zu
|
||||||
|
gestalten. Zeigen Sie Code-Beispiele als formatierte Codeblöcke, fügen Sie Links
|
||||||
|
zu weiterführenden Ressourcen ein, oder binden Sie bei Bedarf Abbildungen ein
|
||||||
|
(etwa ein Diagramm der Datenpipeline, ein Screenshot der Benutzeroberfläche,
|
||||||
|
etc.). Achten Sie dabei auf Dateigrößen und Formate (Bilder als PNG/JPG,
|
||||||
|
Diagramme wenn möglich als SVG für Langlebigkeit). Falls Diagramme der
|
||||||
|
Architektur oder Workflow-Abbildungen hilfreich sind, können diese mit simplen
|
||||||
|
Mitteln erstellt werden (zur Not handgezeichnet und abfotografiert, besser
|
||||||
|
jedoch mit Tools wie mermaid.js Diagrammen in Markdown oder Graphviz). Diese
|
||||||
|
Visualisierungen sind jedoch nur dann einzusetzen, wenn sie echten Mehrwert
|
||||||
|
bieten und ohne komplexe Build-Prozesse eingebunden werden können. Im Zweifel
|
||||||
|
hat textuelle Beschreibung Vorrang, um nicht vom **prinzip “keep it simple”**
|
||||||
|
abzuweichen.
|
||||||
|
|
||||||
|
### Fazit Format und Struktur
|
||||||
|
|
||||||
|
Insgesamt gilt: **Die Dokumentation sollte im gleichen Repository leben wie der
|
||||||
|
Code, klar strukturiert und in einem einfach handhabbaren Format vorliegen.**
|
||||||
|
Sie soll ohne spezielle Umgebung lesbar sein – ein Nutzer, der das Repository
|
||||||
|
klont oder herunterlädt, muss sofort Zugang zur Dokumentation haben. Dieses
|
||||||
|
Prinzip entspricht auch den FAIR- und RSE-Richtlinien, die fordern, Software
|
||||||
|
(und deren Doku) _auffindbar_ und _zugänglich_ zu machen, ohne Hürden. Eine gut
|
||||||
|
gepflegte README in Markdown erfüllt diese Anforderungen in den meisten Fällen
|
||||||
|
optimal.
|
||||||
|
|
||||||
|
## Umfang und Fokus der Dokumentation
|
||||||
|
|
||||||
|
Gerade weil Forschende wenig Zeit haben, muss die Dokumentation **effizient**
|
||||||
|
gestaltet sein – sie soll alle wichtigen Informationen enthalten, aber auch
|
||||||
|
nicht unnötig ausschweifen. Für typische Forschungssoftware-Projekte in den
|
||||||
|
Geisteswissenschaften wird ein Umfang von _maximal ca. 10 Seiten_ (bei Bedarf
|
||||||
|
verteilt auf mehrere Dateien) als ausreichend erachtet. Dieser Richtwert
|
||||||
|
verhindert, dass die Doku zu einer unüberschaubaren Abhandlung wird, und zwingt
|
||||||
|
zur Fokussierung auf das Wesentliche. Wichtig ist der **Inhalt, nicht die
|
||||||
|
Länge**: eine kürzere, aber inhaltsreiche Dokumentation ist besser als eine
|
||||||
|
lange, die nichts aussagt.
|
||||||
|
|
||||||
|
Ein effizienter Umfang lässt sich erreichen, indem man sich auf die oben
|
||||||
|
genannten Kernpunkte konzentriert und Ablenkendes weglässt. Dokumentieren Sie
|
||||||
|
**alles, was für Nachvollziehbarkeit und Wiederverwendung nötig ist, und skippen
|
||||||
|
Sie alles andere**. Zum Beispiel muss nicht jeder interne Programmiertrick
|
||||||
|
erläutert werden – Quellcode-Kommentare richten sich an Entwickler, während die
|
||||||
|
Nutzerdokumentation sich auf Nutzung und Kontext beschränkt. Verzichten Sie auf
|
||||||
|
seitenlange Theorieableitungen (verweisen Sie stattdessen auf Papers) und auf
|
||||||
|
generische Erklärungen bekannter Technologien (man muss Git oder Python nicht in
|
||||||
|
der Doku erklären, sondern kann referenzieren). Halten Sie auch die Sprache
|
||||||
|
prägnant: kurze Absätze, Listen und einfache Sätze erhöhen die Lesbarkeit.
|
||||||
|
Fachtermini aus dem jeweiligen wissenschaftlichen Bereich dürfen verwendet
|
||||||
|
werden, aber erklären Sie sie, falls die Zielnutzer sie evtl. nicht kennen.
|
||||||
|
|
||||||
|
**Priorisierung:** Beginnen Sie mit einer Minimaldokumentation, die alle
|
||||||
|
Schlüsselaspekte abdeckt (_“keine Dokumentation”_ ist keine Option). _Good
|
||||||
|
Enough Practices_ empfehlen, als ersten Schritt zumindest einen **kurzen
|
||||||
|
erklärenden Kommentar am Anfang jedes Scripts** oder eine README mit ein paar
|
||||||
|
Sätzen zu erstellen. Diese Hürde ist niedrig und bringt bereits Nutzen – selbst
|
||||||
|
wenn (noch) keine ausführliche Handbuch-Doku existiert. Später kann die
|
||||||
|
Dokumentation erweitert werden, insbesondere wenn die Software in Kooperation
|
||||||
|
entsteht oder mehr Nutzer gewinnt. Es hat sich gezeigt, dass ausführliche
|
||||||
|
Dokumentation oft erst entsteht, wenn ein echter Bedarf (z. B. durch externe
|
||||||
|
Nutzer) vorhanden ist. Daher: zögern Sie nicht, zunächst _klein_ anzufangen,
|
||||||
|
aber stellen Sie sicher, dass zumindest die kritischen Informationen sofort
|
||||||
|
verfügbar sind (lieber ein 2-seitiges README heute, als das perfekte 30-seitige
|
||||||
|
Handbuch in zwei Jahren, das evtl. nie geschrieben wird).
|
||||||
|
|
||||||
|
Die Obergrenze von \~10 Seiten ist ein Richtwert. Umfangreiche Projekte könnten
|
||||||
|
etwas mehr benötigen, sehr kleine Tools kommen mit einer Seite aus. Das Ziel
|
||||||
|
ist, dass ein interessierter Nutzer die Dokumentation in überschaubarer Zeit
|
||||||
|
durchsehen kann. Ein guter Test ist: **Kann eine neue Person in < 1 Stunde mit
|
||||||
|
Hilfe der Doku das Tool zum Laufen bringen und ein einfaches Beispiel
|
||||||
|
ausführen?** Wenn ja, ist der Detailgrad angemessen. Wenn die Person hingegen
|
||||||
|
nach 10 Seiten immer noch nicht weiß, wie sie loslegen soll, muss die Doku
|
||||||
|
fokussierter werden. Fügen Sie zur Not eine kurze _Übersicht/Zusammenfassung_ am
|
||||||
|
Anfang ein, die das Wichtigste in Kürze nennt – viele Leser entscheiden in
|
||||||
|
wenigen Minuten, ob sie eine Software weiter betrachten oder nicht, und hier
|
||||||
|
zählt der erste Eindruck.
|
||||||
|
|
||||||
|
Ein weiterer Tipp zur Effizienz: Nutzen Sie **Verweise und vorhandene
|
||||||
|
Ressourcen**. Wenn z. B. Ihr Tool auf einem komplizierten Setup (Datenbank,
|
||||||
|
Webserver) aufbaut, brauchen Sie nicht jede Installationsoption im Detail in
|
||||||
|
Ihrer Doku zu reproduzieren – verlinken Sie auf offizielle
|
||||||
|
Installationsanleitungen dieser Abhängigkeiten, und nennen Sie nur Ihre
|
||||||
|
spezifischen Konfigurationen. Ebenso können Tutorials oder Papers, die schon
|
||||||
|
existieren, als weiterführende Links angegeben werden, anstatt Inhalte redundant
|
||||||
|
zu erklären. Das entlastet Ihre Dokumentation und hält sie schlank.
|
||||||
|
|
||||||
|
Zum Fokus gehört auch, zwischen **Nutzerdokumentation und
|
||||||
|
Entwicklerdokumentation** zu unterscheiden. Dieser Katalog adressiert primär die
|
||||||
|
Nutzerdokumentation (für Endnutzer und für die Autoren selbst, wenn sie das Tool
|
||||||
|
später wieder anfassen). Entwicklerdokumentation (z. B. detaillierte
|
||||||
|
API-Dokumente, Code-Kommentare, technische Architektur) kann separat gehalten
|
||||||
|
werden, sofern nötig, um den Hauptnutzerfluss nicht zu überfrachten. Für viele
|
||||||
|
kleine Forschungssoftware-Projekte sind ausführliche Entwicklerdokus nicht nötig
|
||||||
|
– hier reicht es, den Code gut zu kommentieren und eventuell eine grobe
|
||||||
|
Architekturübersicht bereitzustellen. Konzentrieren Sie die Hauptdokumentation
|
||||||
|
darauf, **das Nutzen und Verstehen der Software von außen** zu ermöglichen.
|
||||||
|
|
||||||
|
Abschließend sei betont: Ein kompakter, zielgerichteter Dokumentsatz, der genau
|
||||||
|
die relevanten Infos liefert, erhöht die Wahrscheinlichkeit, dass er
|
||||||
|
**aktualisiert und genutzt** wird. Umfangmonster schrecken ab und veralten
|
||||||
|
schneller. Halten Sie die Dokumentation deshalb so **knapp wie möglich, aber so
|
||||||
|
ausführlich wie nötig** – ganz im Sinne von Einsteins Prinzip, Dinge so einfach
|
||||||
|
wie möglich zu machen, aber nicht einfacher.
|
||||||
|
|
||||||
|
## (Teil-)automatisierte Dokumentationswerkzeuge
|
||||||
|
|
||||||
|
Die Dokumentationslast lässt sich durch den Einsatz geeigneter Werkzeuge
|
||||||
|
erheblich senken. Gerade Forschende, die alleine programmieren, können von
|
||||||
|
**(teil-)automatisierter Dokumentation** profitieren, um konsistente und
|
||||||
|
aktuelle Unterlagen zu erhalten, ohne alles von Hand schreiben zu müssen. Im
|
||||||
|
Folgenden werden einige Tools und Möglichkeiten vorgestellt – samt Empfehlungen,
|
||||||
|
_wann_ ihr Einsatz sinnvoll oder notwendig ist:
|
||||||
|
|
||||||
|
### Docstrings und API-Dokumentationsgeneratoren
|
||||||
|
|
||||||
|
Nutzen Sie die Möglichkeit, Dokumentation _direkt im Quellcode_ unterzubringen,
|
||||||
|
z. B. in Form von **Docstrings** (mehrzeilige Strings in Funktionen/Klassen bei
|
||||||
|
Python, Roxygen-Kommentare in R, Javadoc-Kommentare in Java, etc.). Diese dienen
|
||||||
|
doppelt: Zum einen erleichtern sie es Ihnen und Kollegen, den Code beim Lesen zu
|
||||||
|
verstehen, zum anderen können sie von Tools ausgelesen und zu hübschen
|
||||||
|
API-Dokumentationen verarbeitet werden. Idealerweise dokumentieren Sie _jede
|
||||||
|
wichtige Funktion, Klasse oder Modul_ mit einem kurzen Docstring, der Zweck,
|
||||||
|
Parameter, Rückgaben und ggf. Beispiele enthält. Für kleine Scripte genügen ggf.
|
||||||
|
Modul- oder Abschnittskommentare. Wichtig ist Konsistenz im Stil – halten Sie
|
||||||
|
sich an Konventionen Ihres Ökosystems (z. B. **Google Style Guide** für Python
|
||||||
|
Docstrings oder entsprechende Formatvorgaben für andere Sprachen). Mit Tools wie
|
||||||
|
**Sphinx** (für Python, aber grundsätzlich sprachunabhängig) können aus
|
||||||
|
Docstrings automatisiert Webseiten oder PDF-Handbücher generiert werden. Sphinx
|
||||||
|
liest z. B. die Python-Docstrings und erzeugt daraus strukturiert eine
|
||||||
|
Dokumentation; Erweiterungen wie _napoleon_ erlauben es, Google- oder
|
||||||
|
Numpy-Style-Dokumentation direkt zu verarbeiten.
|
||||||
|
|
||||||
|
::: {.column-margin}
|
||||||
|
|
||||||
|
Ähnliche Generatoren gibt es für nahezu alle Sprachen: **Javadoc** für Java,
|
||||||
|
**Doxygen** für C/C++ (und viele andere Sprachen), **MkDocs** oder _pdoc_ für
|
||||||
|
Python, etc.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Der Einsatz solcher Tools ist besonders dann sinnvoll, wenn Ihre
|
||||||
|
Forschungssoftware über eine _Programmierschnittstelle (API)_ verfügt, die von
|
||||||
|
anderen genutzt werden soll, oder wenn das Projekt größer wird und die interne
|
||||||
|
Struktur komplexer ist. In solchen Fällen kann eine _API-Referenz_ (automatisch
|
||||||
|
aus dem Code erzeugt) eine erhebliche Hilfe sein. **Verpflichtend** wird dieser
|
||||||
|
Ansatz etwa, wenn Sie ein Bibliothekspaket veröffentlichen (z. B. ein R-Package
|
||||||
|
in CRAN oder Python-Package auf PyPI) – dort sind Docstrings und generierte
|
||||||
|
Dokumentation quasi Standard. Für ein einmaliges Analyse-Skript in den Digital
|
||||||
|
Humanities ist eine voll ausgebaute API-Doku vielleicht nicht nötig; hier reicht
|
||||||
|
möglicherweise ein inline kommentierter Code. Doch sobald Funktionen von anderen
|
||||||
|
aufgerufen oder das Projekt von mehreren entwickelt wird, sollte ein
|
||||||
|
Dokumentationstool in Betracht gezogen werden, um den Aufwand gering zu halten
|
||||||
|
und Einheitlichkeit zu gewährleisten.
|
||||||
|
|
||||||
|
### Jupyter Notebooks und literate programming
|
||||||
|
|
||||||
|
Ein mächtiges Werkzeug – gerade in datengetriebenen Geisteswissenschaften – sind
|
||||||
|
**Jupyter Notebooks** bzw. R Markdown Notebooks [@maria2019jupyter]. Diese
|
||||||
|
erlauben es, _ausführbaren Code mit erklärendem Text und Visualisierungen_ in
|
||||||
|
einem Dokument zu vereinen. Für Dokumentationszwecke können Notebooks zweierlei
|
||||||
|
leisten: (1) als **Tutorials/Beispiel-Workflows**, die Nutzer interaktiv
|
||||||
|
nachvollziehen können, und (2) als **Reproduzierbarkeits-Dokumentation** für
|
||||||
|
analytische Prozesse. Wenn Ihre Forschungssoftware z. B. eine Bibliothek ist,
|
||||||
|
könnten Sie ein Notebook bereitstellen, das einen typischen Anwendungsfall
|
||||||
|
durchspielt (inklusive Daten-Loading, Aufruf der Funktionen, Darstellung der
|
||||||
|
Ergebnisse).
|
||||||
|
|
||||||
|
Notebooks senken die Hürde, weil Nutzer direkt experimentieren können, und
|
||||||
|
fördern transparente Forschung, da Code, Ergebnisse und Beschreibung
|
||||||
|
zusammenfließen. Sie sind daher sinnvoll, **wenn der Hauptanwendungsfall die
|
||||||
|
Durchführung von Analysen oder Datenverarbeitungen ist**, die man Schritt für
|
||||||
|
Schritt demonstrieren kann.
|
||||||
|
|
||||||
|
::: {.callout-warning}
|
||||||
|
|
||||||
|
Notebooks erfordern allerdings eine lauffähige Umgebung – das heißt, Sie müssen
|
||||||
|
darauf achten, dass alle Abhängigkeiten im Notebook deklariert sind und die
|
||||||
|
Daten zugänglich sind. Es hat sich gezeigt, dass Notebooks aus Publikationen oft
|
||||||
|
nicht ohne Weiteres laufen, weil Pfade, Datenquellen oder spezielle Umgebungen
|
||||||
|
fehlen. Deshalb: Wenn Sie Notebooks als Doku nutzen, stellen Sie sicher, dass
|
||||||
|
sie _leicht ausführbar_ sind (z. B. durch Bereitstellen von Umgebungsdateien wie
|
||||||
|
`environment.yml` oder Dockerfiles, kleinen Beispieldatensätzen und klaren
|
||||||
|
Anweisungen im Notebook). Ggf. kann man Notebooks auch in reine Markdown/HTML
|
||||||
|
exportieren und dem Repo beilegen, damit zumindest statisch die Inhalte
|
||||||
|
einsehbar sind.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
**Wann sind Notebooks verpflichtend?** – Nie im strengen Sinne, aber sie sind
|
||||||
|
quasi Goldstandard, um wissenschaftliche Analysen nachvollziehbar zu machen. In
|
||||||
|
Projekten, wo es um Data Science Workflows oder interaktive Exploration geht,
|
||||||
|
sollten Notebooks stark erwogen werden, während für ein reines Tool/Script eine
|
||||||
|
gut geschriebene README mit Beispielausgabe ausreichend sein kann.
|
||||||
|
|
||||||
|
### Sphinx/MkDocs/Doxygen (statische Dokumentationswebseiten)
|
||||||
|
|
||||||
|
Für umfangreichere Projekte oder solche mit eigener Website kann es sinnvoll
|
||||||
|
sein, eine **Dokumentationswebsite** zu generieren. Tools wie _Sphinx_ (zusammen
|
||||||
|
mit ReadTheDocs für Hosting) oder _MkDocs_ erlauben es, aus
|
||||||
|
Markdown/reStructuredText-Dateien einen ansprechend formatierten
|
||||||
|
HTML-Dokumentationssatz zu bauen. Der Vorteil ist, dass man eine durchsuchbare,
|
||||||
|
verlinkte Doku bekommt, oft mit schönem Layout und zusätzlicher Navigation. Mit
|
||||||
|
_Continuous Integration_ lassen sich diese Seiten bei jedem Git-Push automatisch
|
||||||
|
aktualisieren. Für die Nachhaltigkeit (ENDINGS-Prinzip) ist wichtig, dass diese
|
||||||
|
Webseiten statisch sind – d.h. sie funktionieren ohne Server-Backends und
|
||||||
|
bleiben auch offline nutzbar. Sphinx erfüllt dies, indem es reine HTML-Seiten
|
||||||
|
erzeugt. Solche Tools sind **sinnvoll, wenn die Dokumentation sehr groß oder
|
||||||
|
öffentlich weit verbreitet** ist – z. B. wenn Ihre Software von vielen genutzt
|
||||||
|
wird und Sie ein professionelles Auftreten wünschen, oder wenn Sie die Doku als
|
||||||
|
PDF veröffentlichen möchten. [In kleinen DH-Projekten ist es oft nicht nötig,
|
||||||
|
extra eine Webseite zu hosten; dennoch kann Sphinx auch lokal HTML/PDF erzeugen,
|
||||||
|
was man dem Repo beilegen kann.]{.aside}
|
||||||
|
|
||||||
|
**Verpflichtend** ist so ein Tool selten, höchstens wenn Förderprogramme oder
|
||||||
|
Journals ein dokumentationsseitiges HTML-Manual verlangen. Wenn Sie jedoch
|
||||||
|
planen, Ihre Software z. B. über Jahre zu pflegen und ggf. einem Journal wie
|
||||||
|
JOSS vorzustellen, dann erwartet die Community meist, dass zumindest eine
|
||||||
|
Sphinx/Doxygen-Doku für die API existiert. Als Daumenregel: ab einer Codebasis >
|
||||||
|
einige tausend Zeilen oder > 5 Module lohnt es sich, eine generierte
|
||||||
|
Dokumentation bereitzustellen, um den Überblick zu behalten.
|
||||||
|
|
||||||
|
### In-Code Hilfefunktionen und CL-Interface Doku
|
||||||
|
|
||||||
|
Falls Ihre Software ein **Command-Line Interface (CLI)** hat, stellen Sie
|
||||||
|
sicher, dass eine eingebaute Hilfe vorhanden ist (z. B. Ausgabe bei `--help`).
|
||||||
|
Viele Nutzer greifen zunächst darauf zurück. Dieses Hilfemenü sollte kurz
|
||||||
|
erläutern, welche Subkommandos oder Optionen existieren. Moderne CLI-Frameworks
|
||||||
|
generieren solche Hilfen oft automatisch aus Ihrem Code (z. B. Click oder
|
||||||
|
argparse in Python erzeugen `--help`-Texte). Nutzen Sie das, um konsistente
|
||||||
|
Infos zu garantieren.
|
||||||
|
|
||||||
|
Für **GUI-Anwendungen** sollten Tooltips, Hilfetexte in der Oberfläche oder
|
||||||
|
zumindest ein kleiner _Help_-Abschnitt im Handbuch vorhanden sein. Diese
|
||||||
|
eingebetteten Hilfen ersetzen keine ausführliche Dokumentation, aber sie senken
|
||||||
|
die Schwelle für alltägliche Fragen.
|
||||||
|
|
||||||
|
### Versionskontrolle und kontinuierliche Dokumentationspflege
|
||||||
|
|
||||||
|
Eine Form der _Teil-Automatisierung_ ist es, die Dokumentation an den
|
||||||
|
Entwicklungs-Workflow zu koppeln. So sollte die Dokumentation im selben
|
||||||
|
Versionskontrollsystem (Git) liegen wie der Code, damit Änderungen synchron
|
||||||
|
nachverfolgt werden. Es empfiehlt sich, bei jedem größeren Code-Update zu
|
||||||
|
prüfen, ob die Doku noch stimmt (das kann man sich z. B. als Punkt in
|
||||||
|
Pull-Request-Reviews notieren oder per Issue-Template abfragen). Für Projekte
|
||||||
|
mit Continuous Integration (CI) kann man sogar automatisierte Checks einrichten,
|
||||||
|
die z. B. prüfen, ob die Doku gebaut werden kann oder ob Docstrings fehlen.
|
||||||
|
Einige CI-Skripte generieren bei jedem Commit eine frische Doku (z. B. mittels
|
||||||
|
Sphinx) und veröffentlichen sie – so ist garantiert, dass _die aktuelle
|
||||||
|
Codeversion immer eine aktuelle Doku hat_. [Dieses Level an Automation ist für
|
||||||
|
kleine Projekte evtl. zu viel, aber das **Prinzip “Dokumentation versionieren”**
|
||||||
|
ist allgemeingültig, um die Entwicklungshistorie konsistent zu halten.]{.aside}
|
||||||
|
|
||||||
|
### Spezialfälle
|
||||||
|
|
||||||
|
In bestimmten Fällen gibt es weitere Werkzeuge: z. B. **Doxygen** für
|
||||||
|
automatisierte Code-Diagramme und Querverweise (gerne in C++-Projekten genutzt),
|
||||||
|
oder **Swagger/OpenAPI** für automatische Dokumentation von Web-APIs. Wenn Ihre
|
||||||
|
Forschungssoftware z. B. einen Webservice anbietet, kann Swagger eine
|
||||||
|
interaktive API-Doku erzeugen. Ebenso können **Literatur-Manager** wie Manubot
|
||||||
|
oder RMarkdown Bücher helfen, Code und Text zu integrieren (aber das geht über
|
||||||
|
das hinaus, was die meisten DH-Projekte benötigen). Erwähnenswert ist noch
|
||||||
|
**Jupyter Book** oder R **Bookdown**, womit man umfangreiche narrative
|
||||||
|
Dokumentationen (inkl. Code) als Website/Book erstellen kann – nützlich, falls
|
||||||
|
Ihre Dokumentation eher ein ausführlicher Lehrtext werden soll (z. B. wenn die
|
||||||
|
Software einen ganzen methodischen Ansatz dokumentiert). Für den hier
|
||||||
|
anvisierten Zweck (knackiger Doku-Katalog) sind solche Tools meist zu
|
||||||
|
schwergewichtig.
|
||||||
|
|
||||||
|
### Wann ist was verpflichtend
|
||||||
|
|
||||||
|
Es gibt kein universelles Muss, außer: **Irgendeine Form der Doku ist Pflicht**.
|
||||||
|
Ob Sie nun per Hand Markdown schreiben oder Sphinx einsetzen, hängt von Kontext
|
||||||
|
und Projektgröße ab. Allgemein gilt: Verwenden Sie Automatisierung wo immer
|
||||||
|
möglich, _um sich zu entlasten_, aber vermeiden Sie Overhead durch Tools, die
|
||||||
|
Sie nicht brauchen. Ein einzelnes historisches Analyse-Skript braucht kein
|
||||||
|
Doxygen; ein komplexes DH-Toolkit mit API sollte hingegen Doxygen oder Sphinx
|
||||||
|
nutzen, damit die Nutzer nicht den Code lesen müssen, um Funktionen zu
|
||||||
|
verstehen. Denken Sie daran: _“Die beste Dokumentation ist die, die sich selbst
|
||||||
|
schreibt.”_ – dieses Motto aus der Literatur spielt darauf an, dass wir Tools
|
||||||
|
nutzen sollen, die uns Schreibarbeit abnehmen. Perfekt autonom schreibt sich die
|
||||||
|
Dokumentation zwar nie, aber moderne Werkzeuge können Routineaufgaben (z. B.
|
||||||
|
Inhaltsverzeichnisse, Funktionsreferenzen, Formatierung) automatisieren. Dadurch
|
||||||
|
bleibt Ihnen mehr Zeit für das inhaltliche Fine-Tuning der Texte.
|
||||||
|
|
||||||
|
## Best Practices, Vorlagen und Checklisten
|
||||||
|
|
||||||
|
Um zu entscheiden, _was_ dokumentiert wird (und was nicht), helfen etablierte
|
||||||
|
**Best Practices** sowie Vorlagen aus der Community. Im Folgenden sind einige
|
||||||
|
bewährte Richtlinien zusammengefasst, untermauert von Quellen, die bei der
|
||||||
|
Priorisierung der Dokumentationsinhalte helfen:
|
||||||
|
|
||||||
|
### Orientierung an Nutzerbedürfnissen
|
||||||
|
|
||||||
|
Stellen Sie sich beim Schreiben der Doku die verschiedenen _Nutzerrollen_ vor:
|
||||||
|
**“Zukünftiges Ich”**, **Kolleg\*innen**, **Fachforscher anderer Disziplin** und
|
||||||
|
ggf. **Software-Entwickler, die den Code erweitern**. Jede dieser Gruppen möchte
|
||||||
|
bestimmte Dinge wissen. _Forscher\*innen_ fragen: _Was kann das Tool? Wie
|
||||||
|
benutze ich es? In welchem Kontext steht es?_. _Entwickler\*innen_ fragen: _Wie
|
||||||
|
kann ich beitragen? Wie funktioniert es unter der Haube?_. Priorisieren Sie
|
||||||
|
zunächst die erstgenannten (Anwender) – deshalb Fokus auf Zweck, Nutzung und
|
||||||
|
Ergebnisse in der Hauptdoku. Detailinfos für Entwickler (z. B. Code-Struktur,
|
||||||
|
To-do-Liste) können separat oder später ergänzt werden. Halten Sie sich stets
|
||||||
|
vor Augen: **Dokumentation ist primär für Menschen** (nicht für Maschinen),
|
||||||
|
daher schreiben Sie klar und vermeiden Sie unnötigen Jargon. _Selbst wenn der
|
||||||
|
Code “für sich spricht”_, denken Sie daran, dass klare Erläuterungen später viel
|
||||||
|
Zeit sparen.
|
||||||
|
|
||||||
|
### Checkliste für die Mindest-Dokumentation
|
||||||
|
|
||||||
|
Die folgenden Punkte fassen zusammen, was eine gute Dokumentation mindestens
|
||||||
|
enthalten sollte. Sie können auch als **Qualitäts-Checkliste** dienen, um Ihre
|
||||||
|
Dokumentation zu überprüfen:
|
||||||
|
|
||||||
|
1. **Zielklärung:** Ist der Zweck der Software klar benannt und der
|
||||||
|
wissenschaftliche _Need_ begründet? (Falls nein, ergänzen: _Warum existiert
|
||||||
|
dieses Tool?_)
|
||||||
|
2. **Installation & Voraussetzungen:** Sind alle Schritte, um die Software
|
||||||
|
lauffähig zu machen, dokumentiert (inkl. Dependencies, evtl. mit
|
||||||
|
Installationsbefehlen)? Ist ersichtlich, welche Umgebung nötig ist (OS,
|
||||||
|
Hardware)?
|
||||||
|
3. **Grundlegende Nutzung:** Gibt es eine Anleitung oder Beispiele, wie man die
|
||||||
|
Software verwendet (Eingabe -> Ausgaben)? Ist mindestens ein typischer
|
||||||
|
Workflow beschrieben, idealerweise mit Beispielinput und -output?
|
||||||
|
4. **Optionen & Schnittstellen:** Falls relevant – sind alle wichtigen
|
||||||
|
Funktionen, Befehlsoptionen oder API-Methoden dokumentiert? (Nicht unbedingt
|
||||||
|
jede intern, aber alles, was ein Nutzer aufrufen könnte). Für APIs: Sind
|
||||||
|
Parameter und Rückgaben erläutert?
|
||||||
|
5. **Validierung & Einschränkungen:** Werden Annahmen und Grenzen der Software
|
||||||
|
genannt? Weiß ein*e Nutzer*in, welche Fälle nicht abgedeckt sind oder worauf
|
||||||
|
zu achten ist (z. B. Datenqualität, maximale Größen)? Transparenz hier
|
||||||
|
verhindert Frustration.
|
||||||
|
6. **Hintergrund & Referenzen:** Sind die wichtigsten konzeptionellen
|
||||||
|
Hintergründe oder Referenzen angegeben? (Z. B. theoretische Grundlagen,
|
||||||
|
Algorithmen, Literaturverweise). Das muss kein Essay sein, aber ein paar
|
||||||
|
Sätze + Referenzen schaffen Vertrauen in die wissenschaftliche Fundierung.
|
||||||
|
7. **Kontakt & Weiterführung:** Ist angegeben, wie man Hilfe bekommt oder Fehler
|
||||||
|
melden kann (Issue-Tracker, E-Mail)? Gibt es Hinweise für Beiträge (falls
|
||||||
|
erwünscht) oder zumindest die Information, wer die Autor\*innen sind?
|
||||||
|
8. **Rechtliches & Zitation:** Liegt die Lizenz bei und wird sie genannt? Sind
|
||||||
|
Infos zum Zitieren der Software vorhanden (z. B. “Bitte zitieren Sie DOI
|
||||||
|
XYZ”)? Das stellt sicher, dass die Software nachnutzbar _und_ akademisch
|
||||||
|
kreditiert wird.
|
||||||
|
9. **Aktualität & Version:** Entspricht die Dokumentation der aktuellen
|
||||||
|
Softwareversion? (Check: Versionsnummern, Datumsangaben). Veraltete Doku kann
|
||||||
|
schlimmer sein als keine – planen Sie also ein, die Doku mit jedem Release
|
||||||
|
kurz zu überprüfen.
|
||||||
|
10. **Konsistenz & Stil:** Wird ein einheitlicher Ton und Stil durchgehalten?
|
||||||
|
(z. B. durchgehende Verwendung gleicher Begriffe für Konzepte, Sprache
|
||||||
|
entweder Deutsch oder Englisch einheitlich je nach Zielgruppe). Kleinliche
|
||||||
|
Fehler (Tippfehler, kaputte Links) sind auszumerzen, da sie Nutzer
|
||||||
|
abschrecken.
|
||||||
|
|
||||||
|
Diese Checkliste kann vor einem “Release” der Software durchgegangen werden,
|
||||||
|
ähnlich einem Review-Prozess (vgl. JOSS Review-Kriterien, die viele dieser
|
||||||
|
Punkte abdecken). Sie hilft zu entscheiden, was noch dokumentiert werden muss
|
||||||
|
und was eventuell weggelassen werden kann. **Alles, was für die obigen Punkte
|
||||||
|
nicht relevant ist, kann man tendenziell aus der Hauptdokumentation
|
||||||
|
herauslassen.** Beispielsweise interne Code-Refaktorierungsdetails oder
|
||||||
|
historische Anekdoten zur Entwicklung gehören eher ins interne Changelog oder in
|
||||||
|
Blog-Posts, nicht in die Nutzerdokumentation.
|
||||||
|
|
||||||
|
### Positiv- und Negativbeispiele studieren
|
||||||
|
|
||||||
|
Ein guter Weg, die eigene Dokumentation zu verbessern, ist ein Blick auf
|
||||||
|
Projekte mit exzellenter Doku. In der _Journal of Open Source Software (JOSS)_
|
||||||
|
oder _Journal of Open Research Software (JORS)_ werden oft Softwareartikel
|
||||||
|
veröffentlicht, bei denen die zugehörigen Repositorien vorbildliche READMEs und
|
||||||
|
Wikis haben. Diese können als Vorlage dienen. Achten Sie darauf, wie diese
|
||||||
|
Projekte ihre README strukturieren, welche Abschnitte vorhanden sind und welche
|
||||||
|
nicht. Viele erfolgreiche Projekte haben z. B. eine ähnliche Reihenfolge:
|
||||||
|
Introduction, Installation, Usage, Contributing, License, Citation – ein Muster,
|
||||||
|
das sich bewährt hat. Ebenso gibt es von Initiativen wie der Software
|
||||||
|
Sustainability Institute Blogposts mit Best Practices und sogar Vorlagen
|
||||||
|
(Templates) für Dokumentation. Nutzen Sie solche Ressourcen; sie ersparen einem
|
||||||
|
das Rad neu zu erfinden. Allerdings: Adaptieren Sie sie auf Ihre Bedürfnisse –
|
||||||
|
nicht jede Vorlage passt 1:1.
|
||||||
|
|
||||||
|
### Prinzipien: FAIR und ENDINGS
|
||||||
|
|
||||||
|
Beachten Sie, dass dieser Anforderungskatalog in Einklang mit den Prinzipien des
|
||||||
|
**Research Software Engineering** und den **ENDINGS-Prinzipien** steht. Gutes
|
||||||
|
Research Software Engineering fördert u.a. _Nachhaltigkeit, Offenheit und
|
||||||
|
Reproduzierbarkeit_ in der Softwareentwicklung. Dementsprechend legt unsere
|
||||||
|
Dokumentations-Checkliste Wert auf Reproduzierbarkeit (Installation, Daten,
|
||||||
|
Beispiele), Offenheit (Lizenz, offene Formate) und Nachhaltigkeit
|
||||||
|
(Versionierung, Langlebigkeit der Doku). Die ENDINGS-Prinzipien für digitale
|
||||||
|
Projekte betonen insbesondere die Bedeutung von Dokumentation für
|
||||||
|
Datenstrukturen, offenen Lizenzen, statischen Outputs und Zitierbarkeit. Unsere
|
||||||
|
Empfehlungen, etwa ein statisches Markdown-README beizulegen, die
|
||||||
|
Datenmodell-Doku nicht auszulagern oder Zitationsangaben zu machen, setzen genau
|
||||||
|
diese Vorgaben um. Indem Sie also diesem Anforderungskatalog folgen,
|
||||||
|
berücksichtigen Sie automatisch wichtige anerkannte Prinzipien für gute
|
||||||
|
wissenschaftliche Softwarepraxis.
|
||||||
|
|
||||||
|
### Kontinuierliche Verbesserung und Feedback
|
||||||
|
|
||||||
|
Dokumentation ist kein einmaliges Ereignis, sondern ein fortlaufender Prozess.
|
||||||
|
Best Practice ist, früh Feedback von Testnutzer\*innen oder Kolleg\*innen
|
||||||
|
einzuholen: Lassen Sie jemanden die Anleitung befolgen und hören Sie auf
|
||||||
|
Stolpersteine. Oft zeigen sich Lücken erst im Praxistest ("Ich wusste nicht, was
|
||||||
|
ich nach Schritt X tun soll" etc.). Planen Sie Zeiten ein, die Dokumentation
|
||||||
|
nachzuführen, insbesondere wenn sich die Software ändert. Ein lebendiges Projekt
|
||||||
|
wird vielleicht Release für Release die Dokumentation erweitern (evtl. neue
|
||||||
|
Tutorials, neue Module dokumentieren). Nutzen Sie auch _Issues_ für
|
||||||
|
Dokumentation: Wenn Nutzer Fragen stellen, überlegen Sie, ob die Antwort in die
|
||||||
|
offizielle Doku übernommen werden sollte. So wächst die Dokumentation organisch
|
||||||
|
entlang der tatsächlichen Bedürfnisse.
|
||||||
|
|
||||||
|
### Zusammenfassung Best Practices
|
||||||
|
|
||||||
|
Zusammenfassend helfen die genannten Best Practices dabei, die Dokumentation
|
||||||
|
**zielgerichtet** zu gestalten: Dokumentiert wird, was dem Verständnis und der
|
||||||
|
Nutzung dient; weggelassen wird, was überflüssig oder selbstverständlich ist.
|
||||||
|
Eine gute Dokumentation _erzählt eine klare Geschichte_ über die Software,
|
||||||
|
anstatt den Leser mit irrelevanten Details zu verlieren. Mit den richtigen
|
||||||
|
Werkzeugen und Prinzipien an der Hand kann selbst unter Zeitdruck eine
|
||||||
|
qualitativ hochwertige Dokumentation entstehen – zur Freude aller, die mit der
|
||||||
|
Forschungssoftware arbeiten möchten.
|
||||||
|
|
||||||
|
## Fazit
|
||||||
|
|
||||||
|
Die hier präsentierten Anforderungen und Empfehlungen bieten einen **Leitfaden
|
||||||
|
für die Dokumentation von Forschungssoftware** in den Digital Humanities. Sie
|
||||||
|
sind darauf ausgerichtet, mit überschaubarem Aufwand maximale
|
||||||
|
**Nachvollziehbarkeit, Langlebigkeit und Wiederverwendbarkeit** zu erreichen.
|
||||||
|
Indem zentrale Inhalte (Ziele, Inputs/Outputs, Hintergrund, etc.) klar
|
||||||
|
dokumentiert, ein nutzerfreundliches Format (README im Repo) gewählt, der Umfang
|
||||||
|
fokussiert gehalten und hilfreiche Tools eingesetzt werden, kann die
|
||||||
|
Dokumentation zur Stärke eines Projekts werden statt einem lästigen Anhängsel.
|
||||||
|
|
||||||
|
Wissenschaftlich fundierte Best Practices – von _Ten Simple Rules for
|
||||||
|
Documenting Scientific Software_ bis zu den _ENDINGS-Principles_ – untermauern
|
||||||
|
diesen Katalog. Die Umsetzung dieser Richtlinien wird dazu beitragen, dass
|
||||||
|
Forschungssoftware aus den Geisteswissenschaften nicht nur kurzfristig von ihren
|
||||||
|
Autor\*innen genutzt wird, sondern langfristig von Dritten verstanden, validiert
|
||||||
|
und weiterentwickelt werden kann. So schließt sich der Kreis zwischen guter
|
||||||
|
**Softwareentwicklung** und guter **Wissenschaft**: Dokumentation ist das
|
||||||
|
Bindeglied, das Code und Erkenntnis transparent verbindet. In der Praxis
|
||||||
|
bedeutet dies zwar zusätzliche Arbeitsschritte, doch wie die Erfahrung zeigt,
|
||||||
|
zahlen sich diese in Form von _Zeiteinsparung bei Nutzern, höherer Zitierbarkeit
|
||||||
|
und größerer Wirkung_ der Software aus. Mit diesem Anforderungskatalog sind
|
||||||
|
Forschende gut gerüstet, um ihre Softwareprojekte dokumentationstechnisch auf
|
||||||
|
ein solides Fundament zu stellen – trotz knapper Zeit und ohne
|
||||||
|
Informatikabschluss. Denn am Ende gilt: **Gut dokumentierte Forschungscode ist
|
||||||
|
nachhaltige Forschung**.
|
||||||
|
|
||||||
|
### Tabellarische Übersicht der Dokumentations-Bestandteile
|
||||||
|
|
||||||
|
::: {.column-page-right}
|
||||||
|
|
||||||
|
Table: _Empfohlene Dokumentationselemente, Inhalte und Umfang._ Diese Übersicht
|
||||||
|
kann als Vorlage dienen, welche Komponenten ein Dokumentationspaket enthalten
|
||||||
|
sollte. Je nach Projekt können einige Elemente wegfallen oder kombiniert werden
|
||||||
|
– entscheidend ist, dass die Kerninformationen (siehe oben) nicht fehlen.
|
||||||
|
|
||||||
|
| **Dokuelement** | **Inhalt/Purpose** | **Format/Ort** | **Umfang** |
|
||||||
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------- |
|
||||||
|
| **README (Hauptdoku)** | Zweck der Software; Kurzbeschreibung; Installationsanleitung; einfaches Nutzungsbeispiel; Lizenz- und Kontaktinfo | Markdown im Root des Repos (statisch versioniert) | 1–2 Seiten |
|
||||||
|
| **Eingabe/Ausgabe-Guide** | Beschreibung der erwarteten Inputs (Datenformat, Parameter) und generierten Outputs (Dateien, Berichte) inkl. Beispielen | Teil der README oder separate Datei (z.B. USAGE.md) | 1 Seite (mit Beispielen) |
|
||||||
|
| **Wissenschaftlicher Hintergrund** | Erläuterung der Methode, Theorie, Algorithmen; Verweise auf Literatur | README-Abschnitt "Hintergrund" oder separate Doku (BACKGROUND.md) | 0.5–1 Seite (plus Referenzen) |
|
||||||
|
| **Bekannte Limitationen** | Auflistung von Einschränkungen, Annahmen, bekannten Problemen; ggf. Workarounds | README-Abschnitt "Limitations" oder FAQ.md | 0.5 Seite |
|
||||||
|
| **Beispiel-Workflow (Tutorial)** | Schritt-für-Schritt Anleitung mit einem realistischen Anwendungsfall (ggf. mit Code und Screenshot) | Jupyter Notebook (`.ipynb`) im Repo `examples/` Ordner oder Markdown in docs/ | 1–3 Seiten / entsprechend Zellen |
|
||||||
|
| **API-Referenz** | Technische Dokumentation von Funktionen/Klassen für Entwickler\*innen | Automatisch generiert aus Docstrings (z.B. Sphinx in `docs/` Ordner, HTML/PDF Ausgabe) | Je nach Codegröße (ggf. umfangreich) |
|
||||||
|
| **CONTRIBUTING** | Anleitung für Beitragswillige: Code Style, Workflow, Tests, Kontakt | CONTRIBUTING.md im Repo | 0.5–1 Seite |
|
||||||
|
| **LICENSE** / **CITATION** | Rechtliche Infos (Lizenztext); Zitationsleitfaden (Bevorzugte Zitierweise, DOI) | Jeweils eigene Datei im Repo (Plain Text/Markdown) | Kurz (Standardtext bzw. Referenz) |
|
||||||
|
| **Release-Information** | Versionshinweise, Änderungsprotokoll (Changelog) | CHANGELOG.md oder Releases auf GitHub | fortlaufend pro Version (Stichpunkte) |
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Schlusswort
|
||||||
|
|
||||||
|
Mit einer solchen Struktur und Herangehensweise lässt sich auch in einem kleinen
|
||||||
|
Forschungsteam eine professionelle Dokumentation erzielen, die den Prinzipien
|
||||||
|
von Open Science und nachhaltiger Softwareentwicklung gerecht wird. Die
|
||||||
|
investierte Mühe wird durch Zeitgewinn bei Wiederverwendung und Erweiterung der
|
||||||
|
Software mehr als aufgewogen. So wird die Forschungssoftware nicht zum
|
||||||
|
einmaligen “Nebenprodukt”, sondern zu einem robusten, teilbaren Ergebnis
|
||||||
|
wissenschaftlicher Arbeit.
|
||||||
|
|
||||||
|
## Methodik / LLMs als 'Autoren' {.appendix}
|
||||||
|
|
||||||
|
Erstellt wurde der initial draft mittels Websuche und "Deep-Research" von
|
||||||
|
`gpt-4.5 (preview)`. Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting
|
||||||
|
durch Nicole Dresselhaus.
|
BIN
Writing/header.png
Normal file
After Width: | Height: | Size: 625 KiB |
106
Writing/ner4all-case-study.bib
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
@misc{ollama_chroma_cookbook,
|
||||||
|
title = {Ollama - Chroma Cookbook},
|
||||||
|
url = {https://cookbook.chromadb.dev/integrations/ollama/embeddings/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = apr
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{smart_connections_plugin,
|
||||||
|
title = {Just wanted to mention that the smart connections plugin is incredible. : r/ObsidianMD},
|
||||||
|
url = {https://www.reddit.com/r/ObsidianMD/comments/1fzmkdk/just_wanted_to_mention_that_the_smart_connections/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = oct
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{khoj_plugin,
|
||||||
|
title = {Khoj: An AI powered Search Assistant for your Second Brain - Share & showcase - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/khoj-an-ai-powered-search-assistant-for-you-second-brain/53756},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2023},
|
||||||
|
month = jul
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{supercharging_obsidian_search,
|
||||||
|
title = {Supercharging Obsidian Search with AI and Ollama},
|
||||||
|
author = {@airabbitX},
|
||||||
|
url = {https://medium.com/@airabbitX/supercharging-obsidian-search-with-local-llms-a-personal-journey-1e008eb649a6},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = nov
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{export_to_common_graph_formats,
|
||||||
|
title = {Export to common graph formats - Plugins ideas - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/export-to-common-graph-formats/4138},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2020},
|
||||||
|
month = feb
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{personal_knowledge_graphs_in_obsidian,
|
||||||
|
title = {Personal Knowledge Graphs in Obsidian},
|
||||||
|
author = {Volodymyr Pavlyshyn},
|
||||||
|
url = {https://volodymyrpavlyshyn.medium.com/personal-knowledge-graphs-in-obsidian-528a0f4584b9},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{export_obsidian_to_rdf,
|
||||||
|
title = {How to export your Obsidian Vault to RDF},
|
||||||
|
author = {Volodymyr Pavlyshyn},
|
||||||
|
url = {https://volodymyrpavlyshyn.medium.com/how-to-export-your-obsidian-vault-to-rdf-00fb2539ed18},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ai_empowered_zettelkasten_with_ner_and_graph_llm,
|
||||||
|
title = {AI empowered Zettelkasten with NER and Graph LLM - Knowledge management - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/ai-empowered-zettelkasten-with-ner-and-graph-llm/79112},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{build_your_second_brain_with_khoj_ai,
|
||||||
|
title = {Build your second brain with Khoj AI},
|
||||||
|
url = {https://dswharshit.medium.com/build-your-second-brain-with-khoj-ai-high-signal-ai-2-87492730d7ce},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = jun
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{second_brain_assistant_with_obsidian,
|
||||||
|
title = {Second Brain Assistant with Obsidian},
|
||||||
|
url = {https://www.ssp.sh/brain/second-brain-assistant-with-obsidian-notegpt/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2025},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{basic_memory_ai_conversations_that_build_knowledge,
|
||||||
|
title = {Basic Memory | AI Conversations That Build Knowledge},
|
||||||
|
url = {https://basicmachines.co/},
|
||||||
|
note = {Accessed: 2025-04-23}
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{local_free_rag_with_question_generation,
|
||||||
|
title = {Local (Free) RAG with Question Generation using LM Studio, Nomic embeddings, ChromaDB and Llama 3.2 on a Mac mini M1},
|
||||||
|
author = {Oscar Galvis},
|
||||||
|
url = {https://lomaky.medium.com/local-free-rag-with-question-generation-using-lm-studio-nomic-embeddings-chromadb-and-llama-3-2-9758877e93b4},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = oct
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{private_gpt_llama_cpp_based_scripts,
|
||||||
|
title = {privateGPT / llama.cpp based scripts},
|
||||||
|
url = {https://www.ssp.sh/brain/second-brain-assistant-with-obsidian-notegpt/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2025},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
613
Writing/ner4all-case-study.md
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Writing
|
||||||
|
- table-wrap
|
||||||
|
authors:
|
||||||
|
- name: GPT-4.5
|
||||||
|
url: https://chatgpt.com
|
||||||
|
affiliation:
|
||||||
|
- name: OpenAI
|
||||||
|
url: https://openai.com
|
||||||
|
- name: cogito-v1-preview
|
||||||
|
url: https://www.deepcogito.com/research/cogito-v1-preview
|
||||||
|
affiliation:
|
||||||
|
- name: DeepCogito
|
||||||
|
url: https://www.deepcogito.com
|
||||||
|
- name: Nicole Dresselhaus
|
||||||
|
affiliation:
|
||||||
|
- name: Humboldt-Universität zu Berlin
|
||||||
|
url: https://hu-berlin.de
|
||||||
|
orcid: 0009-0008-8850-3679
|
||||||
|
date: 2025-05-05
|
||||||
|
categories:
|
||||||
|
- Article
|
||||||
|
- Case-study
|
||||||
|
- ML
|
||||||
|
- NER
|
||||||
|
lang: en
|
||||||
|
citation: true
|
||||||
|
fileClass: authored
|
||||||
|
title: "Case Study: Local LLM-Based NER with n8n and Ollama"
|
||||||
|
abstract: |
|
||||||
|
Named Entity Recognition (NER) is a foundational task in text analysis,
|
||||||
|
traditionally addressed by training NLP models on annotated data. However, a
|
||||||
|
recent study – _“NER4All or Context is All You Need”_ – showed that
|
||||||
|
out-of-the-box Large Language Models (LLMs) can **significantly outperform**
|
||||||
|
classical NER pipelines (e.g. spaCy, Flair) on historical texts by using clever
|
||||||
|
prompting, without any model retraining. This case study demonstrates how to
|
||||||
|
implement the paper’s method using entirely local infrastructure: an **n8n**
|
||||||
|
automation workflow (for orchestration) and a **Ollama** server running a
|
||||||
|
14B-parameter LLM on an NVIDIA A100 GPU. The goal is to enable research
|
||||||
|
engineers and tech-savvy historians to **reproduce and apply this method
|
||||||
|
easily** on their own data, with a focus on usability and correct outputs rather
|
||||||
|
than raw performance.
|
||||||
|
|
||||||
|
We will walk through the end-to-end solution – from accepting a webhook input
|
||||||
|
that defines entity types (e.g. Person, Organization, Location) to prompting a
|
||||||
|
local LLM to extract those entities from a text. The solution covers setup
|
||||||
|
instructions, required infrastructure (GPU, memory, software), model
|
||||||
|
configuration, and workflow design in n8n. We also discuss potential limitations
|
||||||
|
(like model accuracy and context length) and how to address them. By the end,
|
||||||
|
you will have a clear blueprint for a **self-hosted NER pipeline** that
|
||||||
|
leverages the knowledge encoded in LLMs (as advocated by the paper) while
|
||||||
|
maintaining data privacy and reproducibility.
|
||||||
|
|
||||||
|
bibliography:
|
||||||
|
- ner4all-case-study.bib
|
||||||
|
citation-style: springer-humanities-brackets
|
||||||
|
nocite: |
|
||||||
|
@*
|
||||||
|
image: ../thumbs/writing_ner4all-case-study.png
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background: LLM-Based NER Method Overview
|
||||||
|
|
||||||
|
The referenced study introduced a prompt-driven approach to NER, reframing it
|
||||||
|
“from a purely linguistic task into a humanities-focused task”. Instead of
|
||||||
|
training a specialized NER model for each corpus, the method leverages the fact
|
||||||
|
that large pretrained LLMs already contain vast world knowledge and language
|
||||||
|
understanding. The key idea is to **provide the model with contextual
|
||||||
|
definitions and instructions** so it can recognize entities in context. Notably,
|
||||||
|
the authors found that with proper prompts, a commercial LLM (ChatGPT-4) could
|
||||||
|
achieve **precision and recall on par with or better than** state-of-the-art NER
|
||||||
|
tools on a 1921 historical travel guide. This was achieved **zero-shot**, i.e.
|
||||||
|
without any fine-tuning or additional training data beyond the prompt itself.
|
||||||
|
|
||||||
|
**Prompt Strategy:** The success of this approach hinges on careful prompt
|
||||||
|
engineering. The final prompt used in the paper had multiple components:
|
||||||
|
|
||||||
|
- **Persona & Context:** A brief introduction framing the LLM as an _expert_
|
||||||
|
reading a historical text, possibly including domain context (e.g. “This text
|
||||||
|
is an early 20th-century travel guide; language is old-fashioned”). This
|
||||||
|
primes the model with relevant background.
|
||||||
|
- **Task Instructions:** A clear description of the NER task, including the list
|
||||||
|
of entity categories and how to mark them in text. For example: _“Identify all
|
||||||
|
Person (PER), Location (LOC), and Organization (ORG) names in the text and
|
||||||
|
mark each by enclosing it in tags.”_
|
||||||
|
- **Optional Examples:** A few examples of sentences with correct tagged output
|
||||||
|
(few-shot learning) to guide the model. Interestingly, the study found that
|
||||||
|
zero-shot prompting often **outperformed few-shot** until \~16 examples were
|
||||||
|
provided. Given the cost of preparing examples and limited prompt length, our
|
||||||
|
implementation will focus on zero-shot usage for simplicity.
|
||||||
|
- **Reiteration & Emphasis:** The prompt repeated key instructions in different
|
||||||
|
words and emphasized compliance (e.g. _“Make sure you follow the tagging
|
||||||
|
format exactly for every example.”_). This redundancy helps the model adhere
|
||||||
|
to instructions.
|
||||||
|
- **Prompt Engineering Tricks:** They included creative cues to improve
|
||||||
|
accuracy, such as offering a “monetary reward for each correct classification”
|
||||||
|
and the phrase _“Take a deep breath and think step by step.”_. These tricks,
|
||||||
|
drawn from prior work, encouraged the model to be thorough and careful.
|
||||||
|
- **Output Format:** Crucially, the model was asked to **repeat the original
|
||||||
|
text exactly** but insert tags around entity mentions. The authors settled on
|
||||||
|
a format like `<<PER ... /PER>>` to tag people, `<<LOC ... /LOC>>` for
|
||||||
|
locations, etc., covering each full entity span. This inline tagging format
|
||||||
|
leveraged the model’s familiarity with XML/HTML syntax (from its training
|
||||||
|
data) and largely eliminated problems like unclosed tags or extra spaces. By
|
||||||
|
instructing the model _not to alter any other text_, they ensured the output
|
||||||
|
could be easily compared to the input and parsed for entities.
|
||||||
|
|
||||||
|
**Why Local LLMs?** The original experiments used a proprietary API (ChatGPT-4).
|
||||||
|
To make the method accessible to all (and avoid data governance issues of cloud
|
||||||
|
APIs), we implement it with **open-source LLMs running locally**. Recent openly
|
||||||
|
licensed models are rapidly improving and can handle such extraction tasks given
|
||||||
|
the right prompt. Running everything locally also aligns with the paper’s goal
|
||||||
|
of “democratizing access” to NER for diverse, low-resource texts – there are no
|
||||||
|
API costs or internet needed, and data stays on local hardware for privacy.
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
Our solution consists of a **workflow in n8n** that orchestrates the NER
|
||||||
|
process, and a **local Ollama server** that hosts the LLM for text analysis. The
|
||||||
|
high-level workflow is as follows:
|
||||||
|
|
||||||
|
1. **Webhook Trigger (n8n):** A user initiates the process by sending an HTTP
|
||||||
|
request to n8n’s webhook with two inputs: (a) a simple text defining the
|
||||||
|
entity categories of interest (for example, `"PER, ORG, LOC"`), and (b) the
|
||||||
|
text to analyze (either included in the request or accessible via a provided
|
||||||
|
file URL). This trigger node captures the input and starts the automation.
|
||||||
|
2. **Prompt Construction (n8n):** The workflow builds a structured prompt for
|
||||||
|
the LLM. Based on the webhook input, it prepares the system instructions
|
||||||
|
listing each entity type and guidelines, then appends the user’s text.
|
||||||
|
Essentially, n8n will merge the _entity definitions_ into a pre-defined
|
||||||
|
prompt template (the one derived from the paper’s method). This can be done
|
||||||
|
using a **Function node** or an **LLM Prompt node** in n8n to ensure the text
|
||||||
|
and instructions are combined correctly.
|
||||||
|
3. **LLM Inference (Ollama + LLM):** n8n then passes the prompt to an **Ollama
|
||||||
|
Chat Model node**, which communicates with the Ollama server’s API. The
|
||||||
|
Ollama daemon hosts the selected 14B model on the local GPU and returns the
|
||||||
|
model’s completion. In our case, the completion will be the original text
|
||||||
|
with NER tags inserted around the entities (e.g.
|
||||||
|
`<<PER John Doe /PER>> went to <<LOC Berlin /LOC>> ...`). This step harnesses
|
||||||
|
the A100 GPU to generate results quickly, using the chosen model’s weights
|
||||||
|
locally.
|
||||||
|
4. **Output Processing (n8n):** The tagged text output from the LLM can be
|
||||||
|
handled in two ways. The simplest is to **return the tagged text directly**
|
||||||
|
as the response to the webhook call – allowing the user to see their original
|
||||||
|
text with all entities highlighted by tags. Alternatively, n8n can
|
||||||
|
post-process the tags to extract a structured list of entities (e.g. a JSON
|
||||||
|
array of `{"entity": "John Doe", "type": "PER"}`{.json} objects). This
|
||||||
|
parsing can be done with a Regex or code node, but given our focus on
|
||||||
|
correctness, we often trust the model’s tagging format to be consistent (the
|
||||||
|
paper reported the format was reliably followed when instructed clearly).
|
||||||
|
Finally, an **HTTP Response** node sends the results back to the user (or
|
||||||
|
stores them), completing the workflow.
|
||||||
|
|
||||||
|
**Workflow Structure:** In n8n’s interface, the workflow might look like a
|
||||||
|
sequence of connected nodes: **Webhook → Function (build prompt) → AI Model
|
||||||
|
(Ollama) → Webhook Response**. If using n8n’s new AI Agent feature, some steps
|
||||||
|
(like prompt templating) can be configured within the AI nodes themselves. The
|
||||||
|
key is that the Ollama model node is configured to use the local server (usually
|
||||||
|
at `http://127.0.0.1:11434` by default) and the specific model name. We assume
|
||||||
|
the base pipeline (available on GitHub) already includes most of this structure
|
||||||
|
– our task is to **slot in the custom prompt and model configuration** for the
|
||||||
|
NER use case.
|
||||||
|
|
||||||
|
## Setup and Infrastructure Requirements
|
||||||
|
|
||||||
|
To reproduce this solution, you will need a machine with an **NVIDIA GPU** and
|
||||||
|
the following software components installed:
|
||||||
|
|
||||||
|
- **n8n (v1.**x** or later)** – the workflow automation tool. You can install
|
||||||
|
n8n via npm, Docker, or use the desktop app. For a server environment, Docker
|
||||||
|
is convenient. For example, to run n8n with Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm \
|
||||||
|
-p 5678:5678 \
|
||||||
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
|
n8nio/n8n:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This exposes n8n on `http://localhost:5678` for the web interface. (If you use
|
||||||
|
Docker and plan to connect to a host-running Ollama, start the container with
|
||||||
|
`--network=host` to allow access to the Ollama API on localhost.)
|
||||||
|
|
||||||
|
- **Ollama (v0.x\*)** – an LLM runtime that serves models via an HTTP API.
|
||||||
|
Installing Ollama is straightforward: download the installer for your OS from
|
||||||
|
the official site (Linux users can run the one-line script
|
||||||
|
`curl -sSL https://ollama.com/install.sh | sh`). After installation, start the
|
||||||
|
Ollama server (daemon) by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This will launch the service listening on port 11434. You can verify it’s
|
||||||
|
running by opening `http://localhost:11434` in a browser – it should respond
|
||||||
|
with “Ollama is running”. _Note:_ Ensure your system has recent NVIDIA drivers
|
||||||
|
and CUDA support if using GPU. Ollama supports NVIDIA GPUs with compute
|
||||||
|
capability ≥5.0 (the A100 is well above this). Use `nvidia-smi` to confirm
|
||||||
|
your GPU is recognized. If everything is set up, Ollama will automatically use
|
||||||
|
the GPU for model inference (falling back to CPU if none available).
|
||||||
|
|
||||||
|
- **LLM Model (14B class):** Finally, download at least one large language model
|
||||||
|
to use for NER. You have a few options here, and you can “pull” them via
|
||||||
|
Ollama’s CLI:
|
||||||
|
|
||||||
|
- _DeepSeek-R1 14B:_ A 14.8B-parameter model distilled from larger reasoning
|
||||||
|
models (based on Qwen architecture). It’s optimized for reasoning tasks and
|
||||||
|
compares to OpenAI’s models in quality. Pull it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama pull deepseek-r1:14b
|
||||||
|
```
|
||||||
|
|
||||||
|
This downloads \~9 GB of data (the quantized weights). If you have a very
|
||||||
|
strong GPU (e.g. A100 80GB), you could even try `deepseek-r1:70b` (\~43 GB),
|
||||||
|
but 14B is a good balance for our use-case. DeepSeek-R1 is licensed MIT and
|
||||||
|
designed to run locally with no restrictions.
|
||||||
|
|
||||||
|
- _Cogito 14B:_ A 14B “hybrid reasoning” model by Deep Cogito, known for
|
||||||
|
excellent instruction-following and multilingual capability. Pull it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama pull cogito:14b
|
||||||
|
```
|
||||||
|
|
||||||
|
Cogito-14B is also \~9 GB (quantized) and supports an extended context
|
||||||
|
window up to **128k tokens** – which is extremely useful if you plan to
|
||||||
|
analyze very long documents without chunking. It’s trained in 30+ languages
|
||||||
|
and tuned to follow complex instructions, which can help in structured
|
||||||
|
output tasks like ours.
|
||||||
|
|
||||||
|
- _Others:_ Ollama offers many models (LLaMA 2 variants, Mistral, etc.). For
|
||||||
|
instance, `ollama pull llama2:13b` would get a LLaMA-2 13B model. These can
|
||||||
|
work, but for best results in NER with no fine-tuning, we suggest using one
|
||||||
|
of the above well-instructed models. If your hardware is limited, you could
|
||||||
|
try a 7-8B model (e.g., `deepseek-r1:7b` or `cogito:8b`), which download
|
||||||
|
faster and use \~4–5 GB VRAM, at the cost of some accuracy. In CPU-only
|
||||||
|
scenarios, even a 1.5B model is available – it will run very slowly and
|
||||||
|
likely miss more entities, but it proves the pipeline can work on minimal
|
||||||
|
hardware.
|
||||||
|
|
||||||
|
**Hardware Requirements:** Our case assumes an NVIDIA A100 GPU (40 GB), which
|
||||||
|
comfortably hosts a 14B model in memory and accelerates inference. In practice,
|
||||||
|
any modern GPU with ≥10 GB memory can run a 13–14B model in 4-bit quantization.
|
||||||
|
For example, an RTX 3090 or 4090 (24 GB) could handle it, and even smaller GPUs
|
||||||
|
(or Apple Silicon with 16+ GB RAM) can run 7B models. Ensure you have sufficient
|
||||||
|
**system RAM** as well (at least as much as the model size, plus overhead for
|
||||||
|
n8n – 16 GB RAM is a safe minimum for 14B). Disk space of \~10 GB per model is
|
||||||
|
needed. If using Docker for n8n, allocate CPU and memory generously to avoid
|
||||||
|
bottlenecks when the LLM node processes large text.
|
||||||
|
|
||||||
|
## Building the n8n Workflow
|
||||||
|
|
||||||
|
With the environment ready, we now construct the n8n workflow that ties
|
||||||
|
everything together. We outline each component with instructions:
|
||||||
|
|
||||||
|
### 1. Webhook Input for Entities and Text
|
||||||
|
|
||||||
|
Start by creating a **Webhook trigger** node in n8n. This will provide a URL
|
||||||
|
(endpoint) that you can send a request to. Configure it to accept a POST request
|
||||||
|
containing the necessary inputs. For example, we expect the request JSON to look
|
||||||
|
like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entities": "PER, ORG, LOC",
|
||||||
|
"text": "John Doe visited Berlin in 1921 and met with the Board of Acme Corp."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `"entities"` is a simple comma-separated string of entity types (you could
|
||||||
|
also accept an array or a more detailed schema; for simplicity we use the format
|
||||||
|
used in the paper: PER for person, LOC for location, ORG for organization). The
|
||||||
|
`"text"` field contains the content to analyze. In a real scenario, the text
|
||||||
|
could be much longer or might be sent as a file. If it's a file, one approach is
|
||||||
|
to send it as form-data and use n8n’s **Read Binary File** + **Move Binary
|
||||||
|
Data** nodes to get it into text form. Alternatively, send a URL in the JSON and
|
||||||
|
use an HTTP Request node in the workflow to fetch the content. The key is that
|
||||||
|
by the end of this step, we have the raw text and the list of entity labels
|
||||||
|
available in the n8n workflow as variables.
|
||||||
|
|
||||||
|
### 2. Constructing the LLM Prompt
|
||||||
|
|
||||||
|
Next, add a node to build the prompt that will be fed to the LLM. You can use a
|
||||||
|
**Function** node (JavaScript code) or the **“Set” node** to template a prompt
|
||||||
|
string. We will create two pieces of prompt content: a **system instruction**
|
||||||
|
(the role played by the system prompt in chat models) and the **user message**
|
||||||
|
(which will contain the text to be processed).
|
||||||
|
|
||||||
|
According to the method, our **system prompt** should incorporate the following:
|
||||||
|
|
||||||
|
- **Persona/Context:** e.g. _“You are a historian and archivist analyzing a
|
||||||
|
historical document. The language may be old or have archaic spellings. You
|
||||||
|
have extensive knowledge of people, places, and organizations relevant to the
|
||||||
|
context.”_ This establishes domain expertise in the model.
|
||||||
|
- **Task Definition:** e.g. _“Your task is to perform Named Entity Recognition.
|
||||||
|
Identify all occurrences of the specified entity types in the given text and
|
||||||
|
annotate them with the corresponding tags.”_
|
||||||
|
- **Entity Definitions:** List the entity categories provided by the user, with
|
||||||
|
a brief definition if needed. For example: _“The entity types are: PER
|
||||||
|
(persons or fictional characters), ORG (organizations, companies,
|
||||||
|
institutions), LOC (locations such as cities, countries, landmarks).”_ If the
|
||||||
|
user already provided definitions in the webhook, include those; otherwise a
|
||||||
|
generic definition as shown is fine.
|
||||||
|
- **Tagging Instructions:** Clearly explain the tagging format. We adopt the
|
||||||
|
format from the paper: each entity should be wrapped in `<<TYPE ... /TYPE>>`.
|
||||||
|
So instruct: _“Enclose each entity in double angle brackets with its type
|
||||||
|
label. For example: <\<PER John Doe /PER>> for a person named John Doe. Do not
|
||||||
|
alter any other text – only insert tags. Ensure every opening tag has a
|
||||||
|
closing tag.”_ Also mention that tags can nest or overlap if necessary (though
|
||||||
|
that’s rare).
|
||||||
|
- **Output Expectations:** Emphasize that the output should be the **exact
|
||||||
|
original text, verbatim, with tags added** and nothing else. For example:
|
||||||
|
_“Repeat the input text exactly, adding the tags around the entities. Do not
|
||||||
|
add explanations or remove any content. The output should look like the
|
||||||
|
original text with markup.”_ This is crucial to prevent the model from
|
||||||
|
omitting or rephrasing text. The paper’s prompt literally had a line: “Repeat
|
||||||
|
the given text exactly. Be very careful to ensure that nothing is added or
|
||||||
|
removed apart from the annotations.”.
|
||||||
|
- **Compliance & Thoughtfulness:** We can borrow the trick of telling the model
|
||||||
|
to take its time and be precise. For instance: _“Before answering, take a deep
|
||||||
|
breath and think step by step. Make sure you find **all** entities. You will
|
||||||
|
be rewarded for each correct tag.”_ While the notion of reward is
|
||||||
|
hypothetical, such phrasing has been observed to sharpen the model’s focus.
|
||||||
|
This is optional but can be useful for complex texts.
|
||||||
|
|
||||||
|
Once this system prompt is assembled as a single string, it will be sent as the
|
||||||
|
system role content to the LLM. Now, for the **user prompt**, we simply supply
|
||||||
|
the text to be analyzed. In many chat-based LLMs, the user message would contain
|
||||||
|
the text on which the assistant should perform the task. We might prefix it with
|
||||||
|
something like “Text to analyze:\n” for clarity, or just include the raw text.
|
||||||
|
(Including a prefix is slightly safer to distinguish it from any instructions,
|
||||||
|
but since the system prompt already set the task, the user message can be just
|
||||||
|
the document text.)
|
||||||
|
|
||||||
|
In n8n, if using the **Basic LLM Chain** node, you can configure it to use a
|
||||||
|
custom system prompt. For example, connect the Function/Set node output into the
|
||||||
|
LLM node, and in the LLM node’s settings choose “Mode: Complete” or similar,
|
||||||
|
then under **System Instructions** put an expression that references the
|
||||||
|
constructed prompt text (e.g., `{{ $json["prompt"] }}` if the prompt was output
|
||||||
|
to that field). The **User Message** can similarly be fed from the input text
|
||||||
|
field (e.g., `{{ $json["text"] }}`). Essentially, we map our crafted instruction
|
||||||
|
into the system role, and the actual content into the user role.
|
||||||
|
|
||||||
|
### 3. Configuring the Local LLM (Ollama Model Node)
|
||||||
|
|
||||||
|
Now configure the LLM node to use the **Ollama** backend and your downloaded
|
||||||
|
model. n8n provides an “Ollama Chat Model” integration, which is a sub-node of
|
||||||
|
the AI Agent system. In the n8n editor, add or open the LLM node (if using the
|
||||||
|
AI Agent, this might be inside a larger agent node), and look for model
|
||||||
|
selection. Select **Ollama** as the provider. You’ll need to set up a credential
|
||||||
|
for Ollama API access – use `http://127.0.0.1:11434` as the host (instead of the
|
||||||
|
default localhost, to avoid any IPv6 binding issues). No API key is needed since
|
||||||
|
it’s local. Once connected, you should see a dropdown of available models (all
|
||||||
|
the ones you pulled). Choose the 14B model you downloaded, e.g.
|
||||||
|
`deepseek-r1:14b` or `cogito:14b`.
|
||||||
|
|
||||||
|
Double-check the **parameters** for generation. By default, Ollama models have
|
||||||
|
their own preset for max tokens and temperature. For an extraction task, we want
|
||||||
|
the model to stay **focused and deterministic**. It’s wise to set a relatively
|
||||||
|
low temperature (e.g. 0.2) to reduce randomness, and a high max tokens so it can
|
||||||
|
output the entire text with tags (set max tokens to at least the length of your
|
||||||
|
input in tokens plus 10-20% for tags). If using Cogito with its 128k context,
|
||||||
|
you can safely feed very long text; with other models (often \~4k context),
|
||||||
|
ensure your text isn’t longer than the model’s context limit or use a model
|
||||||
|
variant with extended context. If the model supports **“tools” or functions**,
|
||||||
|
you won’t need those here – this is a single-shot prompt, not a multi-step agent
|
||||||
|
requiring tool usage, so just the chat completion mode is sufficient.
|
||||||
|
|
||||||
|
At this point, when the workflow runs to this node, n8n will send the system and
|
||||||
|
user messages to Ollama and wait for the response. The heavy lifting is done by
|
||||||
|
the LLM on the GPU, which will generate the tagged text. On an A100, a 14B model
|
||||||
|
can process a few thousand tokens of input and output in just a handful of
|
||||||
|
seconds (exact time depends on the model and input size).
|
||||||
|
|
||||||
|
### 4. Returning the Results
|
||||||
|
|
||||||
|
After the LLM node, add a node to handle the output. If you want to present the
|
||||||
|
**tagged text** directly, you can pass the LLM’s output to the final Webhook
|
||||||
|
Response node (or if using the built-in n8n chat UI, you would see the answer in
|
||||||
|
the chat). The tagged text will look something like:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
<<PER John Doe /PER>> visited <<LOC Berlin /LOC>> in 1921 and met with the Board
|
||||||
|
of <<ORG Acme Corp /ORG>>.
|
||||||
|
```
|
||||||
|
|
||||||
|
This format highlights each identified entity. It is immediately human-readable
|
||||||
|
with the tags, and trivial to post-process if needed. For example, one could use
|
||||||
|
a regex like `<<(\w+) (.*?) /\1>>` to extract all `type` and `entity` pairs from
|
||||||
|
the text. In n8n, a quick approach is to use a **Function** node to find all
|
||||||
|
matches of that pattern in `item.json["data"]` (assuming the LLM output is in
|
||||||
|
`data`). Then one could return a JSON array of entities. However, since our
|
||||||
|
focus is on correctness and ease, you might simply return the marked-up text and
|
||||||
|
perhaps document how to parse it externally if the user wants structured data.
|
||||||
|
|
||||||
|
Finally, use an **HTTP Response** node (if the workflow was triggered by a
|
||||||
|
Webhook) to send back the results. If the workflow was triggered via n8n’s chat
|
||||||
|
trigger (in the case of interactive usage), you would instead rely on the chat
|
||||||
|
UI output. For a pure API workflow, the HTTP response will contain either the
|
||||||
|
tagged text or a JSON of extracted entities, which the user’s script or
|
||||||
|
application can then use.
|
||||||
|
|
||||||
|
**Note:** If you plan to run multiple analyses or have an ongoing service, you
|
||||||
|
might want to **persist the Ollama server** (don’t shut it down between runs)
|
||||||
|
and perhaps keep the model loaded in VRAM for performance. Ollama will cache the
|
||||||
|
model in memory after the first request, so subsequent requests are faster. On
|
||||||
|
an A100, you could even load two models (if you plan to experiment with which
|
||||||
|
gives better results) but be mindful of VRAM usage if doing so concurrently.
|
||||||
|
|
||||||
|
## Model Selection Considerations
|
||||||
|
|
||||||
|
We provided two example 14B models (DeepSeek-R1 and Cogito) to use with this
|
||||||
|
pipeline. Both are good choices, but here are some considerations and
|
||||||
|
alternatives:
|
||||||
|
|
||||||
|
- **Accuracy vs. Speed:** Larger models (like 14B or 30B) generally produce more
|
||||||
|
accurate and coherent results, especially for complex instructions, compared
|
||||||
|
to 7B models. Since our aim is correctness of NER output, the A100 allows us
|
||||||
|
to use a 14B model which offers a sweet spot. In preliminary tests, these
|
||||||
|
models can correctly tag most obvious entities and even handle some tricky
|
||||||
|
cases (e.g. person names with titles, organizations that sound like person
|
||||||
|
names, etc.) thanks to their pretrained knowledge. If you find the model is
|
||||||
|
making mistakes, you could try a bigger model (Cogito 32B or 70B, if resources
|
||||||
|
permit). Conversely, if you need faster responses and are willing to trade
|
||||||
|
some accuracy, a 7-8B model or running the 14B at a higher quantization (e.g.
|
||||||
|
4-bit) on CPU might be acceptable for smaller texts.
|
||||||
|
- **Domain of the Text:** The paper dealt with historical travel guide text
|
||||||
|
(1920s era). These open models have been trained on large internet corpora, so
|
||||||
|
they likely have seen a lot of historical names and terms, but their coverage
|
||||||
|
might not be as exhaustive as GPT-4. If your text is in a specific domain
|
||||||
|
(say, ancient mythology or very obscure local history), the model might miss
|
||||||
|
entities that it doesn’t recognize as famous. The prompt’s context can help
|
||||||
|
(for example, adding a note like _“Note: Mythological characters should be
|
||||||
|
considered PERSON entities.”_ as they did for Greek gods). For extremely
|
||||||
|
domain-specific needs, one could fine-tune a model or use a specialized one,
|
||||||
|
but that moves beyond the zero-shot philosophy.
|
||||||
|
- **Language:** If your texts are not in English, ensure the chosen model is
|
||||||
|
multilingual. Cogito, for instance, was trained in over 30 languages, so it
|
||||||
|
can handle many European languages (the paper also tested German prompts). If
|
||||||
|
using a model that’s primarily English (like some LLaMA variants), you might
|
||||||
|
get better results by writing the instructions in English but letting it
|
||||||
|
output tags in the original text. The study found English prompts initially
|
||||||
|
gave better recall even on German text, but with prompt tweaks the gap closed.
|
||||||
|
For our pipeline, you can simply provide the definitions in English and the
|
||||||
|
text in the foreign language – a capable model will still tag the foreign
|
||||||
|
entities. For example, Cogito or DeepSeek should tag a German sentence’s
|
||||||
|
_“Herr Schmidt”_ as `<<PER Herr Schmidt /PER>>`. Always test on a small sample
|
||||||
|
if in doubt.
|
||||||
|
- **Extended Context:** If your input text is very long (tens of thousands of
|
||||||
|
words), you should chunk it into smaller segments (e.g. paragraph by
|
||||||
|
paragraph) and run the model on each, then merge the outputs. This is because
|
||||||
|
most models (including DeepSeek 14B) have a context window of 2048–8192
|
||||||
|
tokens. However, Cogito’s 128k context capability is a game-changer – in
|
||||||
|
theory you could feed an entire book and get a single output. Keep in mind the
|
||||||
|
time and memory usage will grow with very large inputs, and n8n might need
|
||||||
|
increased timeout settings for such long runs. For typical use (a few pages of
|
||||||
|
text at a time), the standard context is sufficient.
|
||||||
|
|
||||||
|
In our implementation, we encourage experimenting with both DeepSeek-R1 and
|
||||||
|
Cogito models. Both are **open-source and free for commercial use** (Cogito uses
|
||||||
|
an Apache 2.0 license, DeepSeek MIT). They represent some of the best 14B-class
|
||||||
|
models as of early 2025. You can cite these models in any academic context if
|
||||||
|
needed, or even switch to another model with minimal changes to the n8n workflow
|
||||||
|
(just pull the model and change the model name in the Ollama node).
|
||||||
|
|
||||||
|
## Example Run
|
||||||
|
|
||||||
|
Let’s run through a hypothetical example to illustrate the output. Suppose a
|
||||||
|
historian supplies the following via the webhook:
|
||||||
|
|
||||||
|
- **Entities:** `PER, ORG, LOC`
|
||||||
|
- **Text:** _"Baron Münchhausen was born in Bodenwerder and served in the
|
||||||
|
Russian military under Empress Anna. Today, the Münchhausen Museum in
|
||||||
|
Bodenwerder is operated by the town council."_
|
||||||
|
|
||||||
|
When the workflow executes, the LLM receives instructions to tag people (PER),
|
||||||
|
organizations (ORG), and locations (LOC). With the prompt techniques described,
|
||||||
|
the model’s output might look like:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
<<PER Baron Münchhausen /PER>> was born in <<LOC Bodenwerder /LOC>> and served
|
||||||
|
in the Russian military under <<PER Empress Anna /PER>>. Today, the <<ORG
|
||||||
|
Münchhausen Museum /ORG>> in <<LOC Bodenwerder /LOC>> is operated by the town
|
||||||
|
council.
|
||||||
|
```
|
||||||
|
|
||||||
|
All person names (Baron Münchhausen, Empress Anna) are enclosed in `<<PER>>`
|
||||||
|
tags, the museum is marked as an organization, and the town Bodenwerder is
|
||||||
|
marked as a location (twice). The rest of the sentence remains unchanged. This
|
||||||
|
output can be returned as-is to the user. They can visually verify it or
|
||||||
|
programmatically parse out the tagged entities. The correctness of outputs is
|
||||||
|
high: each tag corresponds to a real entity mention in the text, and there are
|
||||||
|
no hallucinated tags. If the model were to make an error (say, tagging "Russian"
|
||||||
|
as LOC erroneously), the user could adjust the prompt (for example, clarify that
|
||||||
|
national adjectives are not entities) and re-run.
|
||||||
|
|
||||||
|
## Limitations and Solutions
|
||||||
|
|
||||||
|
While this pipeline makes NER easier to reproduce, it’s important to be aware of
|
||||||
|
its limitations and how to mitigate them:
|
||||||
|
|
||||||
|
- **Model Misclassifications:** A local 14B model may not match GPT-4’s level of
|
||||||
|
understanding. It might occasionally tag something incorrectly or miss a
|
||||||
|
subtle entity. For instance, in historical texts, titles or honorifics (e.g.
|
||||||
|
_“Dr. John Smith”_) might confuse it, or a ship name might be tagged as ORG
|
||||||
|
when it’s not in our categories. **Solution:** Refine the prompt with
|
||||||
|
additional guidance. You can add a “Note” section in the instructions to
|
||||||
|
handle known ambiguities (the paper did this with notes about Greek gods being
|
||||||
|
persons, etc.). Also, a quick manual review or spot-check is recommended for
|
||||||
|
important outputs. Since the output format is simple, a human or a simple
|
||||||
|
script can catch obvious mistakes (e.g., if "Russian" was tagged LOC, a
|
||||||
|
post-process could remove it knowing it's likely wrong). Over time, if you
|
||||||
|
notice a pattern of mistakes, update the prompt instructions accordingly.
|
||||||
|
|
||||||
|
- **Text Reproduction Issues:** We instruct the model to output the original
|
||||||
|
text verbatim with tags, but LLMs sometimes can’t resist minor changes. They
|
||||||
|
may “correct” spelling or punctuation, or alter spacing. The paper noted this
|
||||||
|
tendency and used fuzzy matching when evaluating. In our pipeline, minor
|
||||||
|
format changes usually don’t harm the extraction, but if preserving text
|
||||||
|
exactly is important (say for downstream alignment), this is a concern.
|
||||||
|
**Solution:** Emphasize fidelity in the prompt (we already do). If needed, do
|
||||||
|
a diff between the original text and tagged text and flag differences. Usually
|
||||||
|
differences will be small (e.g., changing an old spelling to modern). You can
|
||||||
|
then either accept them or attempt a more rigid approach (like asking for a
|
||||||
|
JSON list of entity offsets – though that introduces other complexities and
|
||||||
|
was intentionally avoided by the authors). In practice, we found the tag
|
||||||
|
insertion approach with strong instructions yields nearly identical text apart
|
||||||
|
from the tags.
|
||||||
|
|
||||||
|
- **Long Inputs and Memory:** Very large documents may exceed the model’s input
|
||||||
|
capacity or make the process slow. The A100 GPU can handle a lot, but n8n
|
||||||
|
itself might have default timeouts for a single workflow execution.
|
||||||
|
**Solution:** For long texts, break the input into smaller chunks (maybe one
|
||||||
|
chapter or section at a time). n8n can loop through chunks using the Split In
|
||||||
|
Batches node or simply by splitting the text in the Function node and feeding
|
||||||
|
the LLM node multiple times. You’d then concatenate the outputs. If chunking,
|
||||||
|
ensure that if an entity spans a chunk boundary, it might be missed – usually
|
||||||
|
rare in well-chosen chunk boundaries (paragraph or sentence). Alternatively,
|
||||||
|
use Cogito for its extended context to avoid chunking. Make sure to increase
|
||||||
|
n8n’s execution timeout if needed (via environment variable
|
||||||
|
`N8N_DEFAULT_TIMEOUT`{.bash} or in the workflow settings).
|
||||||
|
|
||||||
|
- **Concurrent Usage:** If multiple users or processes hit the webhook
|
||||||
|
simultaneously, they would be sharing the single LLM instance. Ollama can
|
||||||
|
queue requests, but the GPU will handle them one at a time (unless running
|
||||||
|
separate instances with multiple GPUs). For a research setting with one user
|
||||||
|
at a time, this is fine. If offering this as a service to others, consider
|
||||||
|
queuing requests or scaling out (multiple replicas of this workflow on
|
||||||
|
different GPU machines). The stateless design of the prompt makes each run
|
||||||
|
independent.
|
||||||
|
|
||||||
|
- **n8n Learning Curve:** For historians new to n8n, setting up the workflow
|
||||||
|
might be unfamiliar. However, n8n’s no-code interface is fairly intuitive with
|
||||||
|
a bit of guidance. This case study provides the logic; one can also import
|
||||||
|
pre-built workflows. In fact, the _n8n_ community has template workflows (for
|
||||||
|
example, a template for chatting with local LLMs) that could be adapted. We
|
||||||
|
assume the base pipeline from the paper’s authors is available on GitHub –
|
||||||
|
using that as a starting point, one mostly needs to adjust nodes as described.
|
||||||
|
If needed, one can refer to n8n’s official docs or community forum for help on
|
||||||
|
creating a webhook or using function nodes. Once set up, running the workflow
|
||||||
|
is as easy as sending an HTTP request or clicking “Execute Workflow” in n8n.
|
||||||
|
|
||||||
|
- **Output Verification:** Since we prioritize correctness, you may want to
|
||||||
|
evaluate how well the model did, especially if you have ground truth
|
||||||
|
annotations. While benchmarking is out of scope here, note that you can
|
||||||
|
integrate evaluation into the pipeline too. For instance, if you had a small
|
||||||
|
test set with known entities, you could compare the model output tags with
|
||||||
|
expected tags using a Python script (n8n has an Execute Python node) or use an
|
||||||
|
NER evaluation library like _nervaluate_ for precision/recall. This is exactly
|
||||||
|
what the authors did to report performance, and you could mimic that to gauge
|
||||||
|
your chosen model’s accuracy.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
By following this guide, we implemented the **NER4All** paper’s methodology with
|
||||||
|
a local, reproducible setup. We used n8n to handle automation and prompt
|
||||||
|
assembly, and a local LLM (via Ollama) to perform the heavy-duty language
|
||||||
|
understanding. The result is a flexible NER pipeline that requires **no training
|
||||||
|
data or API access** – just a well-crafted prompt and a powerful pretrained
|
||||||
|
model. We demonstrated how a user can specify custom entity types and get their
|
||||||
|
text annotated in one click or API call. The approach leverages the strengths of
|
||||||
|
LLMs (vast knowledge and language proficiency) to adapt to historical or niche
|
||||||
|
texts, aligning with the paper’s finding that a bit of context and expert prompt
|
||||||
|
design can unlock high NER performance.
|
||||||
|
|
||||||
|
Importantly, this setup is **easy to reproduce**: all components are either
|
||||||
|
open-source or freely available (n8n, Ollama, and the models). A research
|
||||||
|
engineer or historian can run it on a single machine with sufficient resources,
|
||||||
|
and it can be shared as a workflow file for others to import. By removing the
|
||||||
|
need for extensive data preparation or model training, this lowers the barrier
|
||||||
|
to extracting structured information from large text archives.
|
||||||
|
|
||||||
|
Moving forward, users can extend this case study in various ways: adding more
|
||||||
|
entity types (just update the definitions input), switching to other LLMs as
|
||||||
|
they become available (perhaps a future 20B model with even better
|
||||||
|
understanding), or integrating the output with databases or search indexes for
|
||||||
|
further analysis. With the rapid advancements in local AI models, we anticipate
|
||||||
|
that such pipelines will become even more accurate and faster over time,
|
||||||
|
continually democratizing access to advanced NLP for all domains.
|
||||||
|
|
||||||
|
**Sources:** This implementation draws on insights from Ahmed et al. (2025) for
|
||||||
|
the prompt-based NER method, and uses tools like n8n and Ollama as documented in
|
||||||
|
their official guides. The chosen models (DeepSeek-R1 and Cogito) are described
|
||||||
|
in their respective releases. All software and models are utilized in accordance
|
||||||
|
with their licenses for a fully local deployment.
|
||||||
|
|
||||||
|
## Methodik / LLMs als 'Autoren' {.appendix}
|
||||||
|
|
||||||
|
Erstellt wurde der initial draft mittels Websuche und "Deep-Research" von
|
||||||
|
`gpt-4.5 (preview)`. Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting
|
||||||
|
durch Nicole Dresselhaus.
|
106
Writing/obsidian-rag.bib
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
@misc{ollama_chroma_cookbook,
|
||||||
|
title = {Ollama - Chroma Cookbook},
|
||||||
|
url = {https://cookbook.chromadb.dev/integrations/ollama/embeddings/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = apr
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{smart_connections_plugin,
|
||||||
|
title = {Just wanted to mention that the smart connections plugin is incredible. : r/ObsidianMD},
|
||||||
|
url = {https://www.reddit.com/r/ObsidianMD/comments/1fzmkdk/just_wanted_to_mention_that_the_smart_connections/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = oct
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{khoj_plugin,
|
||||||
|
title = {Khoj: An AI powered Search Assistant for your Second Brain - Share & showcase - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/khoj-an-ai-powered-search-assistant-for-you-second-brain/53756},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2023},
|
||||||
|
month = jul
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{supercharging_obsidian_search,
|
||||||
|
title = {Supercharging Obsidian Search with AI and Ollama},
|
||||||
|
author = {@airabbitX},
|
||||||
|
url = {https://medium.com/@airabbitX/supercharging-obsidian-search-with-local-llms-a-personal-journey-1e008eb649a6},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = nov
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{export_to_common_graph_formats,
|
||||||
|
title = {Export to common graph formats - Plugins ideas - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/export-to-common-graph-formats/4138},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2020},
|
||||||
|
month = feb
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{personal_knowledge_graphs_in_obsidian,
|
||||||
|
title = {Personal Knowledge Graphs in Obsidian},
|
||||||
|
author = {Volodymyr Pavlyshyn},
|
||||||
|
url = {https://volodymyrpavlyshyn.medium.com/personal-knowledge-graphs-in-obsidian-528a0f4584b9},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{export_obsidian_to_rdf,
|
||||||
|
title = {How to export your Obsidian Vault to RDF},
|
||||||
|
author = {Volodymyr Pavlyshyn},
|
||||||
|
url = {https://volodymyrpavlyshyn.medium.com/how-to-export-your-obsidian-vault-to-rdf-00fb2539ed18},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ai_empowered_zettelkasten_with_ner_and_graph_llm,
|
||||||
|
title = {AI empowered Zettelkasten with NER and Graph LLM - Knowledge management - Obsidian Forum},
|
||||||
|
url = {https://forum.obsidian.md/t/ai-empowered-zettelkasten-with-ner-and-graph-llm/79112},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{build_your_second_brain_with_khoj_ai,
|
||||||
|
title = {Build your second brain with Khoj AI},
|
||||||
|
url = {https://dswharshit.medium.com/build-your-second-brain-with-khoj-ai-high-signal-ai-2-87492730d7ce},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = jun
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{second_brain_assistant_with_obsidian,
|
||||||
|
title = {Second Brain Assistant with Obsidian},
|
||||||
|
url = {https://www.ssp.sh/brain/second-brain-assistant-with-obsidian-notegpt/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2025},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{basic_memory_ai_conversations_that_build_knowledge,
|
||||||
|
title = {Basic Memory | AI Conversations That Build Knowledge},
|
||||||
|
url = {https://basicmachines.co/},
|
||||||
|
note = {Accessed: 2025-04-23}
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{local_free_rag_with_question_generation,
|
||||||
|
title = {Local (Free) RAG with Question Generation using LM Studio, Nomic embeddings, ChromaDB and Llama 3.2 on a Mac mini M1},
|
||||||
|
author = {Oscar Galvis},
|
||||||
|
url = {https://lomaky.medium.com/local-free-rag-with-question-generation-using-lm-studio-nomic-embeddings-chromadb-and-llama-3-2-9758877e93b4},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2024},
|
||||||
|
month = oct
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{private_gpt_llama_cpp_based_scripts,
|
||||||
|
title = {privateGPT / llama.cpp based scripts},
|
||||||
|
url = {https://www.ssp.sh/brain/second-brain-assistant-with-obsidian-notegpt/},
|
||||||
|
note = {Accessed: 2025-04-23},
|
||||||
|
year = {2025},
|
||||||
|
month = mar
|
||||||
|
}
|
||||||
|
|
421
Writing/springer-humanities-brackets.csl
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only" default-locale="en-US">
|
||||||
|
<info>
|
||||||
|
<title>Springer - Humanities (numeric, brackets)</title>
|
||||||
|
<id>http://www.zotero.org/styles/springer-humanities-brackets</id>
|
||||||
|
<link href="http://www.zotero.org/styles/springer-humanities-brackets" rel="self"/>
|
||||||
|
<link href="http://www.zotero.org/styles/springer-humanities-author-date" rel="template"/>
|
||||||
|
<link href="www.springer.com/cda/content/document/cda_downloaddocument/Key_Style_Points_Aug2012.pdf" rel="documentation"/>
|
||||||
|
<author>
|
||||||
|
<name>Sebastian Karcher</name>
|
||||||
|
</author>
|
||||||
|
<contributor>
|
||||||
|
<name>Julian Onions</name>
|
||||||
|
<email>julian.onions@gmail.com</email>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Richard Karnesky</name>
|
||||||
|
<email>karnesky+zotero@gmail.com</email>
|
||||||
|
<uri>http://arc.nucapt.northwestern.edu/Richard_Karnesky</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Charles Parnot</name>
|
||||||
|
<email>charles.parnot@gmail.com</email>
|
||||||
|
<uri>http://twitter.com/cparnot</uri>
|
||||||
|
</contributor>
|
||||||
|
<category citation-format="numeric"/>
|
||||||
|
<category field="generic-base"/>
|
||||||
|
<category field="humanities"/>
|
||||||
|
<summary>Style for Springer's humanities journals - the journals do look slightly different from each other, but this should work quite closely</summary>
|
||||||
|
<updated>2019-10-01T00:21:45+00:00</updated>
|
||||||
|
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||||
|
</info>
|
||||||
|
<locale>
|
||||||
|
<terms>
|
||||||
|
<term name="container-author" form="verb">by</term>
|
||||||
|
</terms>
|
||||||
|
</locale>
|
||||||
|
<macro name="secondary-contributors">
|
||||||
|
<choose>
|
||||||
|
<if type="chapter paper-conference" match="none">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<choose>
|
||||||
|
<if variable="author">
|
||||||
|
<names variable="editor">
|
||||||
|
<label form="verb" text-case="capitalize-first" suffix=" " plural="never"/>
|
||||||
|
<name and="text" delimiter=", "/>
|
||||||
|
</names>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<if variable="author editor" match="any">
|
||||||
|
<names variable="translator">
|
||||||
|
<label form="verb" text-case="capitalize-first" suffix=" " plural="never"/>
|
||||||
|
<name and="text" delimiter=", "/>
|
||||||
|
</names>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="container-contributors">
|
||||||
|
<choose>
|
||||||
|
<if type="chapter paper-conference" match="any">
|
||||||
|
<group prefix=", " delimiter=", ">
|
||||||
|
<choose>
|
||||||
|
<if variable="author">
|
||||||
|
<names variable="container-author editor" delimiter=", ">
|
||||||
|
<label form="short" suffix=" " plural="never"/>
|
||||||
|
<name and="text" delimiter=", " initialize="false" initialize-with=". "/>
|
||||||
|
</names>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<if variable="author editor" match="any">
|
||||||
|
<names variable="translator">
|
||||||
|
<label form="short" plural="never" suffix=" "/>
|
||||||
|
<name and="text" delimiter=", "/>
|
||||||
|
</names>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="recipient">
|
||||||
|
<choose>
|
||||||
|
<if type="personal_communication">
|
||||||
|
<choose>
|
||||||
|
<if variable="genre">
|
||||||
|
<text variable="genre" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text term="letter" text-case="capitalize-first"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<names variable="recipient" delimiter=", ">
|
||||||
|
<label form="verb" prefix=" " suffix=" "/>
|
||||||
|
<name and="text" delimiter=", "/>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="contributors">
|
||||||
|
<names variable="author">
|
||||||
|
<name and="text" name-as-sort-order="first" sort-separator=", " delimiter=", " delimiter-precedes-last="always"/>
|
||||||
|
<label form="short" plural="never" prefix=", "/>
|
||||||
|
<substitute>
|
||||||
|
<names variable="editor"/>
|
||||||
|
<names variable="translator"/>
|
||||||
|
<text macro="title"/>
|
||||||
|
</substitute>
|
||||||
|
</names>
|
||||||
|
<text macro="recipient"/>
|
||||||
|
</macro>
|
||||||
|
<macro name="interviewer">
|
||||||
|
<names variable="interviewer" delimiter=", ">
|
||||||
|
<label form="verb" prefix=" " text-case="capitalize-first" suffix=" "/>
|
||||||
|
<name and="text" delimiter=", "/>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="archive">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<text variable="archive_location" text-case="capitalize-first"/>
|
||||||
|
<text variable="archive"/>
|
||||||
|
<text variable="archive-place"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="access">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<choose>
|
||||||
|
<if type="graphic report" match="any">
|
||||||
|
<text macro="archive"/>
|
||||||
|
</if>
|
||||||
|
<else-if type="article-magazine article-newspaper bill book chapter graphic legal_case legislation motion_picture paper-conference report song thesis" match="none">
|
||||||
|
<text macro="archive"/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
<text variable="DOI" prefix="https://doi.org/"/>
|
||||||
|
<choose>
|
||||||
|
<if variable="DOI issued" match="none">
|
||||||
|
<choose>
|
||||||
|
<if variable="URL accessed" match="all">
|
||||||
|
<choose>
|
||||||
|
<if type="legal_case" match="none">
|
||||||
|
<text variable="URL"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="accessed" text-case="capitalize-first"/>
|
||||||
|
<date variable="accessed" delimiter=" ">
|
||||||
|
<date-part name="month"/>
|
||||||
|
<date-part name="day"/>
|
||||||
|
</date>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
<else-if type="webpage">
|
||||||
|
<date variable="issued" delimiter=" ">
|
||||||
|
<date-part name="month"/>
|
||||||
|
<date-part name="day"/>
|
||||||
|
</date>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="title">
|
||||||
|
<choose>
|
||||||
|
<if variable="title" match="none">
|
||||||
|
<choose>
|
||||||
|
<if type="personal_communication" match="none">
|
||||||
|
<text variable="genre" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
<else-if type="bill book graphic legal_case legislation motion_picture report song" match="any">
|
||||||
|
<text variable="title" font-style="italic"/>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<text variable="title"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="edition">
|
||||||
|
<choose>
|
||||||
|
<if type="bill book graphic legal_case legislation motion_picture report song" match="any">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="edition">
|
||||||
|
<group delimiter=" " prefix=". ">
|
||||||
|
<number variable="edition" form="ordinal"/>
|
||||||
|
<text term="edition" form="short"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="edition" prefix=". "/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
<else-if type="chapter paper-conference" match="any">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="edition">
|
||||||
|
<group delimiter=" " prefix=", ">
|
||||||
|
<number variable="edition" form="ordinal"/>
|
||||||
|
<text term="edition" form="short"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="edition" prefix=", "/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="locators">
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal">
|
||||||
|
<text variable="volume" prefix=" "/>
|
||||||
|
</if>
|
||||||
|
<else-if type="legal_case">
|
||||||
|
<text variable="volume" prefix=", "/>
|
||||||
|
<text variable="container-title" prefix=" "/>
|
||||||
|
<text variable="page" prefix=" "/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="bill book graphic legal_case legislation motion_picture report song" match="any">
|
||||||
|
<group prefix=". " delimiter=". ">
|
||||||
|
<group>
|
||||||
|
<text term="volume" form="short" text-case="capitalize-first" suffix=" "/>
|
||||||
|
<number variable="volume" form="numeric"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<number variable="number-of-volumes" form="numeric"/>
|
||||||
|
<text term="volume" form="short" prefix=" " plural="true"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="chapter paper-conference" match="any">
|
||||||
|
<choose>
|
||||||
|
<if variable="page" match="none">
|
||||||
|
<group prefix=". ">
|
||||||
|
<text term="volume" form="short" text-case="capitalize-first" suffix=" "/>
|
||||||
|
<number variable="volume" form="numeric"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="locators-chapter">
|
||||||
|
<choose>
|
||||||
|
<if type="chapter paper-conference" match="any">
|
||||||
|
<choose>
|
||||||
|
<if variable="page">
|
||||||
|
<group prefix=", ">
|
||||||
|
<text variable="volume" suffix=":"/>
|
||||||
|
<text variable="page"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="locators-article">
|
||||||
|
<choose>
|
||||||
|
<if type="article-newspaper">
|
||||||
|
<group prefix=", " delimiter=", ">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text variable="edition"/>
|
||||||
|
<text term="edition"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<text term="section" form="short" suffix=" "/>
|
||||||
|
<text variable="section"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else-if type="article-journal">
|
||||||
|
<text variable="page" prefix=": "/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="container-prefix">
|
||||||
|
<text term="in" text-case="capitalize-first"/>
|
||||||
|
</macro>
|
||||||
|
<macro name="container-title">
|
||||||
|
<choose>
|
||||||
|
<if type="chapter paper-conference" match="any">
|
||||||
|
<text macro="container-prefix" suffix=" "/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<if type="legal_case" match="none">
|
||||||
|
<text variable="container-title" font-style="italic"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="publisher">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text variable="publisher-place"/>
|
||||||
|
<text variable="publisher"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="date">
|
||||||
|
<choose>
|
||||||
|
<if variable="issued">
|
||||||
|
<date variable="issued">
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
</if>
|
||||||
|
<else-if variable="accessed">
|
||||||
|
<date variable="accessed">
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="day-month">
|
||||||
|
<date variable="issued">
|
||||||
|
<date-part name="month"/>
|
||||||
|
<date-part name="day" prefix=" "/>
|
||||||
|
</date>
|
||||||
|
</macro>
|
||||||
|
<macro name="collection-title">
|
||||||
|
<text variable="collection-title" text-case="title"/>
|
||||||
|
<text variable="collection-number" prefix=" "/>
|
||||||
|
</macro>
|
||||||
|
<macro name="event">
|
||||||
|
<group>
|
||||||
|
<text term="presented at" suffix=" "/>
|
||||||
|
<text variable="event"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="description">
|
||||||
|
<choose>
|
||||||
|
<if type="interview">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<text macro="interviewer"/>
|
||||||
|
<text variable="medium" text-case="capitalize-first"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="medium" text-case="capitalize-first" prefix=". "/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<if variable="title" match="none"/>
|
||||||
|
<else-if type="thesis"/>
|
||||||
|
<else>
|
||||||
|
<group delimiter=" " prefix=". ">
|
||||||
|
<text variable="genre" text-case="capitalize-first"/>
|
||||||
|
<choose>
|
||||||
|
<if type="report">
|
||||||
|
<text variable="number"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<!--This is for computer programs only. Localization new to 1.0.1, so may be missing in many locales-->
|
||||||
|
<group delimiter=" " prefix=" (" suffix=")">
|
||||||
|
<text term="version"/>
|
||||||
|
<text variable="version"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="issue">
|
||||||
|
<choose>
|
||||||
|
<if type="legal_case">
|
||||||
|
<text variable="authority" prefix=". "/>
|
||||||
|
</if>
|
||||||
|
<else-if type="speech">
|
||||||
|
<group prefix=" " delimiter=", ">
|
||||||
|
<text macro="event"/>
|
||||||
|
<text macro="day-month"/>
|
||||||
|
<text variable="event-place"/>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="article-newspaper article-magazine" match="any">
|
||||||
|
<text macro="day-month" prefix=", "/>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<group prefix=". " delimiter=", ">
|
||||||
|
<choose>
|
||||||
|
<if type="thesis">
|
||||||
|
<text variable="genre" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<citation collapse="citation-number">
|
||||||
|
<sort>
|
||||||
|
<key variable="citation-number"/>
|
||||||
|
</sort>
|
||||||
|
<layout prefix="[" suffix="]" delimiter=", ">
|
||||||
|
<text variable="citation-number"/>
|
||||||
|
</layout>
|
||||||
|
</citation>
|
||||||
|
<bibliography second-field-align="flush" et-al-min="11" et-al-use-first="7" entry-spacing="0">
|
||||||
|
<layout suffix=".">
|
||||||
|
<text variable="citation-number" suffix=". "/>
|
||||||
|
<group delimiter=". ">
|
||||||
|
<text macro="contributors"/>
|
||||||
|
<text macro="date"/>
|
||||||
|
<text macro="title"/>
|
||||||
|
</group>
|
||||||
|
<text macro="description"/>
|
||||||
|
<text macro="secondary-contributors" prefix=". "/>
|
||||||
|
<text macro="container-title" prefix=". "/>
|
||||||
|
<text macro="container-contributors"/>
|
||||||
|
<text macro="edition"/>
|
||||||
|
<text macro="locators-chapter"/>
|
||||||
|
<text macro="locators"/>
|
||||||
|
<text macro="collection-title" prefix=". "/>
|
||||||
|
<text macro="issue"/>
|
||||||
|
<text macro="locators-article"/>
|
||||||
|
<text macro="access" prefix=". "/>
|
||||||
|
</layout>
|
||||||
|
</bibliography>
|
||||||
|
</style>
|
11
_brand.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
color:
|
||||||
|
primary: rgb(162, 70, 130)
|
||||||
|
#logo:
|
||||||
|
# medium: logo.png
|
||||||
|
|
||||||
|
# typography:
|
||||||
|
# fonts:
|
||||||
|
# - family: Jura
|
||||||
|
# source: google
|
||||||
|
# base: Jura
|
||||||
|
# headings: Jura
|
8
_extensions/jmgirard/details/_extension.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
title: Details
|
||||||
|
author: Jeffrey Girard
|
||||||
|
version: 1.0.0
|
||||||
|
quarto-required: ">=1.6.0"
|
||||||
|
contributes:
|
||||||
|
shortcodes:
|
||||||
|
- details.lua
|
||||||
|
|
60
_extensions/jmgirard/details/details.lua
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
-- dtext shortcode
|
||||||
|
function dtext(args, kwargs, meta)
|
||||||
|
local function buildDetails(text, summary, open)
|
||||||
|
local details = {
|
||||||
|
'<p>',
|
||||||
|
'<details' .. open .. '>',
|
||||||
|
'<summary>' .. summary .. '</summary>',
|
||||||
|
'<blockquote>' .. text .. '</blockquote>',
|
||||||
|
'</details>',
|
||||||
|
'</p>'
|
||||||
|
}
|
||||||
|
return table.concat(details, "")
|
||||||
|
end
|
||||||
|
local text = pandoc.utils.stringify(args[1] or 'Add content here.')
|
||||||
|
local summary = (#kwargs["summary"] > 0) and kwargs["summary"] or "Details"
|
||||||
|
local open = ""
|
||||||
|
if table.concat(args, " ", 2):find("open") then
|
||||||
|
open = " open"
|
||||||
|
end
|
||||||
|
local output = buildDetails(text, summary, open)
|
||||||
|
if quarto.doc.isFormat("html:js") then
|
||||||
|
return pandoc.RawInline('html', output)
|
||||||
|
else
|
||||||
|
return pandoc.Null()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dstart shortcode
|
||||||
|
function dstart(args, kwargs, meta)
|
||||||
|
local function buildDetails(summary, open)
|
||||||
|
local details = {
|
||||||
|
'<p>',
|
||||||
|
'<details' .. open .. '>',
|
||||||
|
'<summary>' .. summary .. '</summary>',
|
||||||
|
'<blockquote>'
|
||||||
|
}
|
||||||
|
return table.concat(details, "")
|
||||||
|
end
|
||||||
|
local summary = (#kwargs["summary"] > 0) and kwargs["summary"] or "Details"
|
||||||
|
local open = ""
|
||||||
|
if table.concat(args, " "):find("open") then
|
||||||
|
open = " open"
|
||||||
|
end
|
||||||
|
local output = buildDetails(summary, open)
|
||||||
|
if quarto.doc.isFormat("html:js") then
|
||||||
|
return pandoc.RawInline('html', output)
|
||||||
|
else
|
||||||
|
return pandoc.Null()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dstop shortcode
|
||||||
|
function dstop(args, kwargs, meta)
|
||||||
|
local output = table.concat({'</blockquote>', '</details>', '</p>'}, "")
|
||||||
|
if quarto.doc.isFormat("html:js") then
|
||||||
|
return pandoc.RawInline('html', output)
|
||||||
|
else
|
||||||
|
return pandoc.Null()
|
||||||
|
end
|
||||||
|
end
|
71
_quarto.yml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
project:
|
||||||
|
type: website
|
||||||
|
resources:
|
||||||
|
- thumbs
|
||||||
|
output-dir: dist
|
||||||
|
|
||||||
|
website:
|
||||||
|
title: "Nicole Dresselhaus"
|
||||||
|
site-url: https://nicole.dresselhaus.cloud
|
||||||
|
description: "Ramblings of a madwoman"
|
||||||
|
navbar:
|
||||||
|
search: true
|
||||||
|
left:
|
||||||
|
- href: index.qmd
|
||||||
|
text: Home
|
||||||
|
icon: house
|
||||||
|
- href: About/index.md
|
||||||
|
text: About
|
||||||
|
icon: file-person
|
||||||
|
right:
|
||||||
|
- icon: rss
|
||||||
|
href: index.xml
|
||||||
|
sidebar:
|
||||||
|
style: docked
|
||||||
|
contents:
|
||||||
|
- section: "Serious"
|
||||||
|
contents:
|
||||||
|
- auto: "Writing"
|
||||||
|
- auto: "Coding"
|
||||||
|
- auto: "Health"
|
||||||
|
- auto: "Uni"
|
||||||
|
- text: "---"
|
||||||
|
- section: "Fun"
|
||||||
|
contents:
|
||||||
|
- auto: "Opinions"
|
||||||
|
- auto: "Stuff"
|
||||||
|
- text: "---"
|
||||||
|
- section: "Info"
|
||||||
|
contents:
|
||||||
|
- auto: "About"
|
||||||
|
reader-mode: true
|
||||||
|
draft-mode: unlinked
|
||||||
|
# announcement:
|
||||||
|
# icon: info-circle
|
||||||
|
# dismissable: true
|
||||||
|
# content: "This is complete WIP."
|
||||||
|
# type: primary
|
||||||
|
# position: below-navbar
|
||||||
|
open-graph: true
|
||||||
|
execute:
|
||||||
|
enable: false
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
email-obfuscation: javascript
|
||||||
|
lightbox: true
|
||||||
|
code-overflow: wrap
|
||||||
|
theme:
|
||||||
|
light: [sandstone, styles.scss]
|
||||||
|
dark: [solar, styles.scss]
|
||||||
|
highlight-style:
|
||||||
|
light: espresso.theme
|
||||||
|
dark: gruvbox-dark.theme
|
||||||
|
respect-user-color-scheme: true
|
||||||
|
toc: true
|
||||||
|
toc-location: right # table of contents links (oder rechts)
|
||||||
|
link-external-icon: true # externe links markieren
|
||||||
|
link-external-newwindow: true # externe linkn in neuem Fenster öffnen
|
||||||
|
citation-location: document
|
||||||
|
reference-location: margin # fußnoten im Margin (falls gewünscht)
|
||||||
|
mermaid:
|
||||||
|
theme: default
|
1060
dist/About/Experience.html
vendored
Normal file
1089
dist/About/Extracurricular.html
vendored
Normal file
BIN
dist/About/Nicole_small.png
vendored
Normal file
After Width: | Height: | Size: 311 KiB |
1108
dist/About/Work.html
vendored
Normal file
1107
dist/About/index.html
vendored
Normal file
1097
dist/Coding/Haskell/Advantages.html
vendored
Normal file
1159
dist/Coding/Haskell/Code Snippets/Monoid.html
vendored
Normal file
1214
dist/Coding/Haskell/Code Snippets/Morphisms.html
vendored
Normal file
1067
dist/Coding/Haskell/FFPiH.html
vendored
Normal file
1534
dist/Coding/Haskell/Lenses.html
vendored
Normal file
1251
dist/Coding/Haskell/Webapp-Example/Main.hs.html
vendored
Normal file
1147
dist/Coding/Haskell/Webapp-Example/MyService_Types.hs.html
vendored
Normal file
1556
dist/Coding/Haskell/Webapp-Example/index.html
vendored
Normal file
1074
dist/Health/Issues.html
vendored
Normal file
1077
dist/Opinions/Don't train your own LLM.html
vendored
Normal file
1059
dist/Opinions/Editors.html
vendored
Normal file
1046
dist/Opinions/Keyboard-Layout.html
vendored
Normal file
1075
dist/Stuff/Bielefeldverschwoerung.html
vendored
Normal file
1130
dist/Uni/Lernerfolg_an_der_Uni.html
vendored
Normal file
1604
dist/Writing/Obsidian-RAG.html
vendored
Normal file
14
dist/Writing/RAG für eine Obsidian-Wissensdatenbank: Technische Ansätze/index.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>Redirect</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var redirects = {"":"../Obsidian-RAG.html"};
|
||||||
|
var hash = window.location.hash.startsWith('#') ? window.location.hash.slice(1) : window.location.hash;
|
||||||
|
var redirect = redirects[hash] || redirects[""] || "/";
|
||||||
|
window.document.title = 'Redirect to ' + redirect;
|
||||||
|
window.location.replace(redirect);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
1497
dist/Writing/documentation.html
vendored
Normal file
1421
dist/Writing/ner4all-case-study.html
vendored
Normal file
1531
dist/index.html
vendored
Normal file
7931
dist/index.xml
vendored
Normal file
21
dist/listings.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"listing": "/index.html",
|
||||||
|
"items": [
|
||||||
|
"/Writing/documentation.html",
|
||||||
|
"/Writing/ner4all-case-study.html",
|
||||||
|
"/Writing/Obsidian-RAG.html",
|
||||||
|
"/Coding/Haskell/Webapp-Example/Main.hs.html",
|
||||||
|
"/Coding/Haskell/Webapp-Example/MyService_Types.hs.html",
|
||||||
|
"/Coding/Haskell/Webapp-Example/index.html",
|
||||||
|
"/Health/Issues.html",
|
||||||
|
"/Coding/Haskell/FFPiH.html",
|
||||||
|
"/Coding/Haskell/Lenses.html",
|
||||||
|
"/Coding/Haskell/Code Snippets/Monoid.html",
|
||||||
|
"/Coding/Haskell/Code Snippets/Morphisms.html",
|
||||||
|
"/Coding/Haskell/Advantages.html",
|
||||||
|
"/Uni/Lernerfolg_an_der_Uni.html",
|
||||||
|
"/Stuff/Bielefeldverschwoerung.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
1
dist/robots.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Sitemap: https://nicole.dresselhaus.cloud/sitemap.xml
|
1261
dist/search.json
vendored
Normal file
12
dist/site_libs/bootstrap/bootstrap-dark-6ed95ce66646ab2447a87e45f81c21f3.min.css
vendored
Normal file
12
dist/site_libs/bootstrap/bootstrap-ec71cb1e120c0dd41819aca960e74e38.min.css
vendored
Normal file
2078
dist/site_libs/bootstrap/bootstrap-icons.css
vendored
Normal file
BIN
dist/site_libs/bootstrap/bootstrap-icons.woff
vendored
Normal file
7
dist/site_libs/bootstrap/bootstrap.min.js
vendored
Normal file
7
dist/site_libs/clipboard/clipboard.min.js
vendored
Normal file
275
dist/site_libs/quarto-diagram/mermaid-init.js
vendored
Normal file
13
dist/site_libs/quarto-diagram/mermaid.css
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.mermaidTooltip {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 200px;
|
||||||
|
padding: 2px;
|
||||||
|
font-family: "trebuchet ms", verdana, arial;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #ffffde;
|
||||||
|
border: 1px solid #aaaa33;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
2186
dist/site_libs/quarto-diagram/mermaid.min.js
vendored
Normal file
9
dist/site_libs/quarto-html/anchor.min.js
vendored
Normal file
6
dist/site_libs/quarto-html/popper.min.js
vendored
Normal file
205
dist/site_libs/quarto-html/quarto-syntax-highlighting-6cf5824034cebd0380a5b9c74c43f006.css
vendored
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/* quarto syntax highlight colors */
|
||||||
|
:root {
|
||||||
|
--quarto-hl-al-color: #ffff00;
|
||||||
|
--quarto-hl-an-color: #0066ff;
|
||||||
|
--quarto-hl-at-color: inherit;
|
||||||
|
--quarto-hl-bn-color: #44aa43;
|
||||||
|
--quarto-hl-bu-color: inherit;
|
||||||
|
--quarto-hl-ch-color: #049b0a;
|
||||||
|
--quarto-hl-co-color: #0066ff;
|
||||||
|
--quarto-hl-cn-color: inherit;
|
||||||
|
--quarto-hl-cf-color: #43a8ed;
|
||||||
|
--quarto-hl-dt-color: inherit;
|
||||||
|
--quarto-hl-dv-color: #44aa43;
|
||||||
|
--quarto-hl-do-color: #0066ff;
|
||||||
|
--quarto-hl-er-color: #ffff00;
|
||||||
|
--quarto-hl-ex-color: inherit;
|
||||||
|
--quarto-hl-fl-color: #44aa43;
|
||||||
|
--quarto-hl-fu-color: #ff9358;
|
||||||
|
--quarto-hl-im-color: inherit;
|
||||||
|
--quarto-hl-in-color: #0066ff;
|
||||||
|
--quarto-hl-kw-color: #43a8ed;
|
||||||
|
--quarto-hl-op-color: inherit;
|
||||||
|
--quarto-hl-pp-color: inherit;
|
||||||
|
--quarto-hl-sc-color: #049b0a;
|
||||||
|
--quarto-hl-ss-color: #049b0a;
|
||||||
|
--quarto-hl-st-color: #049b0a;
|
||||||
|
--quarto-hl-va-color: inherit;
|
||||||
|
--quarto-hl-vs-color: #049b0a;
|
||||||
|
--quarto-hl-wa-color: #ffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* other quarto variables */
|
||||||
|
:root {
|
||||||
|
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* syntax highlight based on Pandoc's rules */
|
||||||
|
/* Alert */
|
||||||
|
code span.al {
|
||||||
|
color: #ffff00;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annotation */
|
||||||
|
code span.an {
|
||||||
|
color: #0066ff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute */
|
||||||
|
code span.at {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BaseN */
|
||||||
|
code span.bn {
|
||||||
|
color: #44aa43;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BuiltIn */
|
||||||
|
code span.bu {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ControlFlow */
|
||||||
|
code span.cf {
|
||||||
|
color: #43a8ed;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Char */
|
||||||
|
code span.ch {
|
||||||
|
color: #049b0a;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Constant */
|
||||||
|
code span.cn {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
code span.co {
|
||||||
|
color: #0066ff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation */
|
||||||
|
code span.do {
|
||||||
|
color: #0066ff;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DataType */
|
||||||
|
code span.dt {
|
||||||
|
font-style: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DecVal */
|
||||||
|
code span.dv {
|
||||||
|
color: #44aa43;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
code span.er {
|
||||||
|
color: #ffff00;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extension */
|
||||||
|
code span.ex {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float */
|
||||||
|
code span.fl {
|
||||||
|
color: #44aa43;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Function */
|
||||||
|
code span.fu {
|
||||||
|
color: #ff9358;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import */
|
||||||
|
code span.im {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Information */
|
||||||
|
code span.in {
|
||||||
|
color: #0066ff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyword */
|
||||||
|
code span.kw {
|
||||||
|
color: #43a8ed;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Operator */
|
||||||
|
code span.op {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preprocessor */
|
||||||
|
code span.pp {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialChar */
|
||||||
|
code span.sc {
|
||||||
|
color: #049b0a;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialString */
|
||||||
|
code span.ss {
|
||||||
|
color: #049b0a;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* String */
|
||||||
|
code span.st {
|
||||||
|
color: #049b0a;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variable */
|
||||||
|
code span.va {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VerbatimString */
|
||||||
|
code span.vs {
|
||||||
|
color: #049b0a;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
code span.wa {
|
||||||
|
color: #ffff00;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevent-inlining {
|
||||||
|
content: "</";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=32086ec229fb0849d7fb131b930bf548.css.map */
|
221
dist/site_libs/quarto-html/quarto-syntax-highlighting-dark-2c84ecb840a13f4c7993f9e5648f0c14.css
vendored
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
/* quarto syntax highlight colors */
|
||||||
|
:root {
|
||||||
|
--quarto-hl-kw-color: #ebdbb2;
|
||||||
|
--quarto-hl-fu-color: #689d6a;
|
||||||
|
--quarto-hl-va-color: #458588;
|
||||||
|
--quarto-hl-cf-color: #cc241d;
|
||||||
|
--quarto-hl-op-color: #ebdbb2;
|
||||||
|
--quarto-hl-bu-color: #d65d0e;
|
||||||
|
--quarto-hl-ex-color: #689d6a;
|
||||||
|
--quarto-hl-pp-color: #d65d0e;
|
||||||
|
--quarto-hl-at-color: #d79921;
|
||||||
|
--quarto-hl-ch-color: #b16286;
|
||||||
|
--quarto-hl-sc-color: #b16286;
|
||||||
|
--quarto-hl-st-color: #98971a;
|
||||||
|
--quarto-hl-vs-color: #98971a;
|
||||||
|
--quarto-hl-ss-color: #98971a;
|
||||||
|
--quarto-hl-im-color: #689d6a;
|
||||||
|
--quarto-hl-dt-color: #d79921;
|
||||||
|
--quarto-hl-dv-color: #f67400;
|
||||||
|
--quarto-hl-bn-color: #f67400;
|
||||||
|
--quarto-hl-fl-color: #f67400;
|
||||||
|
--quarto-hl-cn-color: #b16286;
|
||||||
|
--quarto-hl-co-color: #928374;
|
||||||
|
--quarto-hl-do-color: #98971a;
|
||||||
|
--quarto-hl-an-color: #98971a;
|
||||||
|
--quarto-hl-cv-color: #928374;
|
||||||
|
--quarto-hl-re-color: #928374;
|
||||||
|
--quarto-hl-in-color: #282828;
|
||||||
|
--quarto-hl-wa-color: #282828;
|
||||||
|
--quarto-hl-al-color: #282828;
|
||||||
|
--quarto-hl-er-color: #cc241d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* other quarto variables */
|
||||||
|
:root {
|
||||||
|
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* syntax highlight based on Pandoc's rules */
|
||||||
|
pre > code.sourceCode > span {
|
||||||
|
color: #ebdbb2;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.sourceCode > span {
|
||||||
|
color: #ebdbb2;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sourceCode,
|
||||||
|
div.sourceCode pre.sourceCode {
|
||||||
|
color: #ebdbb2;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal */
|
||||||
|
code span {
|
||||||
|
color: #ebdbb2;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
code span.al {
|
||||||
|
color: #282828;
|
||||||
|
background-color: #cc241d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annotation */
|
||||||
|
code span.an {
|
||||||
|
color: #98971a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute */
|
||||||
|
code span.at {
|
||||||
|
color: #d79921;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BaseN */
|
||||||
|
code span.bn {
|
||||||
|
color: #f67400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BuiltIn */
|
||||||
|
code span.bu {
|
||||||
|
color: #d65d0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ControlFlow */
|
||||||
|
code span.cf {
|
||||||
|
color: #cc241d;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Char */
|
||||||
|
code span.ch {
|
||||||
|
color: #b16286;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Constant */
|
||||||
|
code span.cn {
|
||||||
|
color: #b16286;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
code span.co {
|
||||||
|
color: #928374;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CommentVar */
|
||||||
|
code span.cv {
|
||||||
|
color: #928374;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation */
|
||||||
|
code span.do {
|
||||||
|
color: #98971a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DataType */
|
||||||
|
code span.dt {
|
||||||
|
color: #d79921;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DecVal */
|
||||||
|
code span.dv {
|
||||||
|
color: #f67400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
code span.er {
|
||||||
|
color: #cc241d;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extension */
|
||||||
|
code span.ex {
|
||||||
|
color: #689d6a;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float */
|
||||||
|
code span.fl {
|
||||||
|
color: #f67400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Function */
|
||||||
|
code span.fu {
|
||||||
|
color: #689d6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import */
|
||||||
|
code span.im {
|
||||||
|
color: #689d6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Information */
|
||||||
|
code span.in {
|
||||||
|
color: #282828;
|
||||||
|
background-color: #83a598;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyword */
|
||||||
|
code span.kw {
|
||||||
|
color: #ebdbb2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Operator */
|
||||||
|
code span.op {
|
||||||
|
color: #ebdbb2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preprocessor */
|
||||||
|
code span.pp {
|
||||||
|
color: #d65d0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RegionMarker */
|
||||||
|
code span.re {
|
||||||
|
color: #928374;
|
||||||
|
background-color: #1d2021;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialChar */
|
||||||
|
code span.sc {
|
||||||
|
color: #b16286;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialString */
|
||||||
|
code span.ss {
|
||||||
|
color: #98971a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* String */
|
||||||
|
code span.st {
|
||||||
|
color: #98971a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variable */
|
||||||
|
code span.va {
|
||||||
|
color: #458588;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VerbatimString */
|
||||||
|
code span.vs {
|
||||||
|
color: #98971a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
code span.wa {
|
||||||
|
color: #282828;
|
||||||
|
background-color: #fabd2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevent-inlining {
|
||||||
|
content: "</";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=9be8b4365e7d8bb6f535c9d955e89405.css.map */
|
845
dist/site_libs/quarto-html/quarto.js
vendored
Normal file
@ -0,0 +1,845 @@
|
|||||||
|
import * as tabsets from "./tabsets/tabsets.js";
|
||||||
|
|
||||||
|
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
|
||||||
|
detail: {},
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
composed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutMarginEls = () => {
|
||||||
|
// Find any conflicting margin elements and add margins to the
|
||||||
|
// top to prevent overlap
|
||||||
|
const marginChildren = window.document.querySelectorAll(
|
||||||
|
".column-margin.column-container > *, .margin-caption, .aside"
|
||||||
|
);
|
||||||
|
|
||||||
|
let lastBottom = 0;
|
||||||
|
for (const marginChild of marginChildren) {
|
||||||
|
if (marginChild.offsetParent !== null) {
|
||||||
|
// clear the top margin so we recompute it
|
||||||
|
marginChild.style.marginTop = null;
|
||||||
|
const top = marginChild.getBoundingClientRect().top + window.scrollY;
|
||||||
|
if (top < lastBottom) {
|
||||||
|
const marginChildStyle = window.getComputedStyle(marginChild);
|
||||||
|
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
|
||||||
|
const margin = lastBottom - top + marginBottom;
|
||||||
|
marginChild.style.marginTop = `${margin}px`;
|
||||||
|
}
|
||||||
|
const styles = window.getComputedStyle(marginChild);
|
||||||
|
const marginTop = parseFloat(styles["marginTop"]);
|
||||||
|
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.document.addEventListener("DOMContentLoaded", function (_event) {
|
||||||
|
// Recompute the position of margin elements anytime the body size changes
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
const resizeObserver = new window.ResizeObserver(
|
||||||
|
throttle(() => {
|
||||||
|
layoutMarginEls();
|
||||||
|
if (
|
||||||
|
window.document.body.getBoundingClientRect().width < 990 &&
|
||||||
|
isReaderMode()
|
||||||
|
) {
|
||||||
|
quartoToggleReader();
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
);
|
||||||
|
resizeObserver.observe(window.document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
|
||||||
|
const sidebarEl = window.document.getElementById("quarto-sidebar");
|
||||||
|
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
|
||||||
|
const marginSidebarEl = window.document.getElementById(
|
||||||
|
"quarto-margin-sidebar"
|
||||||
|
);
|
||||||
|
// function to determine whether the element has a previous sibling that is active
|
||||||
|
const prevSiblingIsActiveLink = (el) => {
|
||||||
|
const sibling = el.previousElementSibling;
|
||||||
|
if (sibling && sibling.tagName === "A") {
|
||||||
|
return sibling.classList.contains("active");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// dispatch for htmlwidgets
|
||||||
|
// they use slideenter event to trigger resize
|
||||||
|
function fireSlideEnter() {
|
||||||
|
const event = window.document.createEvent("Event");
|
||||||
|
event.initEvent("slideenter", true, true);
|
||||||
|
window.document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.addEventListener("shown.bs.tab", fireSlideEnter);
|
||||||
|
});
|
||||||
|
|
||||||
|
// dispatch for shiny
|
||||||
|
// they use BS shown and hidden events to trigger rendering
|
||||||
|
function distpatchShinyEvents(previous, current) {
|
||||||
|
if (window.jQuery) {
|
||||||
|
if (previous) {
|
||||||
|
window.jQuery(previous).trigger("hidden");
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
window.jQuery(current).trigger("shown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tabby.js listener: Trigger event for htmlwidget and shiny
|
||||||
|
document.addEventListener(
|
||||||
|
"tabby",
|
||||||
|
function (event) {
|
||||||
|
fireSlideEnter();
|
||||||
|
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track scrolling and mark TOC links as active
|
||||||
|
// get table of contents and sidebar (bail if we don't have at least one)
|
||||||
|
const tocLinks = tocEl
|
||||||
|
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
|
||||||
|
: [];
|
||||||
|
const makeActive = (link) => tocLinks[link].classList.add("active");
|
||||||
|
const removeActive = (link) => tocLinks[link].classList.remove("active");
|
||||||
|
const removeAllActive = () =>
|
||||||
|
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
|
||||||
|
|
||||||
|
// activate the anchor for a section associated with this TOC entry
|
||||||
|
tocLinks.forEach((link) => {
|
||||||
|
link.addEventListener("click", () => {
|
||||||
|
if (link.href.indexOf("#") !== -1) {
|
||||||
|
const anchor = link.href.split("#")[1];
|
||||||
|
const heading = window.document.querySelector(
|
||||||
|
`[data-anchor-id="${anchor}"]`
|
||||||
|
);
|
||||||
|
if (heading) {
|
||||||
|
// Add the class
|
||||||
|
heading.classList.add("reveal-anchorjs-link");
|
||||||
|
|
||||||
|
// function to show the anchor
|
||||||
|
const handleMouseout = () => {
|
||||||
|
heading.classList.remove("reveal-anchorjs-link");
|
||||||
|
heading.removeEventListener("mouseout", handleMouseout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add a function to clear the anchor when the user mouses out of it
|
||||||
|
heading.addEventListener("mouseout", handleMouseout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sections = tocLinks.map((link) => {
|
||||||
|
const target = link.getAttribute("data-scroll-target");
|
||||||
|
if (target.startsWith("#")) {
|
||||||
|
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
|
||||||
|
} else {
|
||||||
|
return window.document.querySelector(decodeURI(`${target}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionMargin = 200;
|
||||||
|
let currentActive = 0;
|
||||||
|
// track whether we've initialized state the first time
|
||||||
|
let init = false;
|
||||||
|
|
||||||
|
const updateActiveLink = () => {
|
||||||
|
// The index from bottom to top (e.g. reversed list)
|
||||||
|
let sectionIndex = -1;
|
||||||
|
if (
|
||||||
|
window.innerHeight + window.pageYOffset >=
|
||||||
|
window.document.body.offsetHeight
|
||||||
|
) {
|
||||||
|
// This is the no-scroll case where last section should be the active one
|
||||||
|
sectionIndex = 0;
|
||||||
|
} else {
|
||||||
|
// This finds the last section visible on screen that should be made active
|
||||||
|
sectionIndex = [...sections].reverse().findIndex((section) => {
|
||||||
|
if (section) {
|
||||||
|
return window.pageYOffset >= section.offsetTop - sectionMargin;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sectionIndex > -1) {
|
||||||
|
const current = sections.length - sectionIndex - 1;
|
||||||
|
if (current !== currentActive) {
|
||||||
|
removeAllActive();
|
||||||
|
currentActive = current;
|
||||||
|
makeActive(current);
|
||||||
|
if (init) {
|
||||||
|
window.dispatchEvent(sectionChanged);
|
||||||
|
}
|
||||||
|
init = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inHiddenRegion = (top, bottom, hiddenRegions) => {
|
||||||
|
for (const region of hiddenRegions) {
|
||||||
|
if (top <= region.bottom && bottom >= region.top) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorySelector = "header.quarto-title-block .quarto-category";
|
||||||
|
const activateCategories = (href) => {
|
||||||
|
// Find any categories
|
||||||
|
// Surround them with a link pointing back to:
|
||||||
|
// #category=Authoring
|
||||||
|
try {
|
||||||
|
const categoryEls = window.document.querySelectorAll(categorySelector);
|
||||||
|
for (const categoryEl of categoryEls) {
|
||||||
|
const categoryText = categoryEl.textContent;
|
||||||
|
if (categoryText) {
|
||||||
|
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
|
||||||
|
const linkEl = window.document.createElement("a");
|
||||||
|
linkEl.setAttribute("href", link);
|
||||||
|
for (const child of categoryEl.childNodes) {
|
||||||
|
linkEl.append(child);
|
||||||
|
}
|
||||||
|
categoryEl.appendChild(linkEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function hasTitleCategories() {
|
||||||
|
return window.document.querySelector(categorySelector) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function offsetRelativeUrl(url) {
|
||||||
|
const offset = getMeta("quarto:offset");
|
||||||
|
return offset ? offset + url : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function offsetAbsoluteUrl(url) {
|
||||||
|
const offset = getMeta("quarto:offset");
|
||||||
|
const baseUrl = new URL(offset, window.location);
|
||||||
|
|
||||||
|
const projRelativeUrl = url.replace(baseUrl, "");
|
||||||
|
if (projRelativeUrl.startsWith("/")) {
|
||||||
|
return projRelativeUrl;
|
||||||
|
} else {
|
||||||
|
return "/" + projRelativeUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a meta tag value
|
||||||
|
function getMeta(metaName) {
|
||||||
|
const metas = window.document.getElementsByTagName("meta");
|
||||||
|
for (let i = 0; i < metas.length; i++) {
|
||||||
|
if (metas[i].getAttribute("name") === metaName) {
|
||||||
|
return metas[i].getAttribute("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findAndActivateCategories() {
|
||||||
|
// Categories search with listing only use path without query
|
||||||
|
const currentPagePath = offsetAbsoluteUrl(
|
||||||
|
window.location.origin + window.location.pathname
|
||||||
|
);
|
||||||
|
const response = await fetch(offsetRelativeUrl("listings.json"));
|
||||||
|
if (response.status == 200) {
|
||||||
|
return response.json().then(function (listingPaths) {
|
||||||
|
const listingHrefs = [];
|
||||||
|
for (const listingPath of listingPaths) {
|
||||||
|
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
|
||||||
|
for (const item of listingPath.items) {
|
||||||
|
const encodedItem = encodeURI(item);
|
||||||
|
if (
|
||||||
|
encodedItem === currentPagePath ||
|
||||||
|
encodedItem === currentPagePath + "index.html"
|
||||||
|
) {
|
||||||
|
// Resolve this path against the offset to be sure
|
||||||
|
// we already are using the correct path to the listing
|
||||||
|
// (this adjusts the listing urls to be rooted against
|
||||||
|
// whatever root the page is actually running against)
|
||||||
|
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
|
||||||
|
const baseUrl = window.location;
|
||||||
|
const resolvedPath = new URL(relative, baseUrl);
|
||||||
|
listingHrefs.push(resolvedPath.pathname);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the tree for a nearby linting and use that if we find one
|
||||||
|
const nearestListing = findNearestParentListing(
|
||||||
|
offsetAbsoluteUrl(window.location.pathname),
|
||||||
|
listingHrefs
|
||||||
|
);
|
||||||
|
if (nearestListing) {
|
||||||
|
activateCategories(nearestListing);
|
||||||
|
} else {
|
||||||
|
// See if the referrer is a listing page for this item
|
||||||
|
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
|
||||||
|
const referrerListing = listingHrefs.find((listingHref) => {
|
||||||
|
const isListingReferrer =
|
||||||
|
listingHref === referredRelativePath ||
|
||||||
|
listingHref === referredRelativePath + "index.html";
|
||||||
|
return isListingReferrer;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (referrerListing) {
|
||||||
|
// Try to use the referrer if possible
|
||||||
|
activateCategories(referrerListing);
|
||||||
|
} else if (listingHrefs.length > 0) {
|
||||||
|
// Otherwise, just fall back to the first listing
|
||||||
|
activateCategories(listingHrefs[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasTitleCategories()) {
|
||||||
|
findAndActivateCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNearestParentListing = (href, listingHrefs) => {
|
||||||
|
if (!href || !listingHrefs) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Look up the tree for a nearby linting and use that if we find one
|
||||||
|
const relativeParts = href.substring(1).split("/");
|
||||||
|
while (relativeParts.length > 0) {
|
||||||
|
const path = relativeParts.join("/");
|
||||||
|
for (const listingHref of listingHrefs) {
|
||||||
|
if (listingHref.startsWith(path)) {
|
||||||
|
return listingHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relativeParts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
|
||||||
|
let isVisible = true;
|
||||||
|
let elRect;
|
||||||
|
|
||||||
|
return (hiddenRegions) => {
|
||||||
|
if (el === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last element of the TOC
|
||||||
|
const lastChildEl = el.lastElementChild;
|
||||||
|
|
||||||
|
if (lastChildEl) {
|
||||||
|
// Converts the sidebar to a menu
|
||||||
|
const convertToMenu = () => {
|
||||||
|
for (const child of el.children) {
|
||||||
|
child.style.opacity = 0;
|
||||||
|
child.style.overflow = "hidden";
|
||||||
|
child.style.pointerEvents = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
nexttick(() => {
|
||||||
|
const toggleContainer = window.document.createElement("div");
|
||||||
|
toggleContainer.style.width = "100%";
|
||||||
|
toggleContainer.classList.add("zindex-over-content");
|
||||||
|
toggleContainer.classList.add("quarto-sidebar-toggle");
|
||||||
|
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
|
||||||
|
toggleContainer.id = placeholderDescriptor.id;
|
||||||
|
toggleContainer.style.position = "fixed";
|
||||||
|
|
||||||
|
const toggleIcon = window.document.createElement("i");
|
||||||
|
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
|
||||||
|
toggleIcon.classList.add("bi");
|
||||||
|
toggleIcon.classList.add("bi-caret-down-fill");
|
||||||
|
|
||||||
|
const toggleTitle = window.document.createElement("div");
|
||||||
|
const titleEl = window.document.body.querySelector(
|
||||||
|
placeholderDescriptor.titleSelector
|
||||||
|
);
|
||||||
|
if (titleEl) {
|
||||||
|
toggleTitle.append(
|
||||||
|
titleEl.textContent || titleEl.innerText,
|
||||||
|
toggleIcon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
toggleTitle.classList.add("zindex-over-content");
|
||||||
|
toggleTitle.classList.add("quarto-sidebar-toggle-title");
|
||||||
|
toggleContainer.append(toggleTitle);
|
||||||
|
|
||||||
|
const toggleContents = window.document.createElement("div");
|
||||||
|
toggleContents.classList = el.classList;
|
||||||
|
toggleContents.classList.add("zindex-over-content");
|
||||||
|
toggleContents.classList.add("quarto-sidebar-toggle-contents");
|
||||||
|
for (const child of el.children) {
|
||||||
|
if (child.id === "toc-title") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = child.cloneNode(true);
|
||||||
|
clone.style.opacity = 1;
|
||||||
|
clone.style.pointerEvents = null;
|
||||||
|
clone.style.display = null;
|
||||||
|
toggleContents.append(clone);
|
||||||
|
}
|
||||||
|
toggleContents.style.height = "0px";
|
||||||
|
const positionToggle = () => {
|
||||||
|
// position the element (top left of parent, same width as parent)
|
||||||
|
if (!elRect) {
|
||||||
|
elRect = el.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
toggleContainer.style.left = `${elRect.left}px`;
|
||||||
|
toggleContainer.style.top = `${elRect.top}px`;
|
||||||
|
toggleContainer.style.width = `${elRect.width}px`;
|
||||||
|
};
|
||||||
|
positionToggle();
|
||||||
|
|
||||||
|
toggleContainer.append(toggleContents);
|
||||||
|
el.parentElement.prepend(toggleContainer);
|
||||||
|
|
||||||
|
// Process clicks
|
||||||
|
let tocShowing = false;
|
||||||
|
// Allow the caller to control whether this is dismissed
|
||||||
|
// when it is clicked (e.g. sidebar navigation supports
|
||||||
|
// opening and closing the nav tree, so don't dismiss on click)
|
||||||
|
const clickEl = placeholderDescriptor.dismissOnClick
|
||||||
|
? toggleContainer
|
||||||
|
: toggleTitle;
|
||||||
|
|
||||||
|
const closeToggle = () => {
|
||||||
|
if (tocShowing) {
|
||||||
|
toggleContainer.classList.remove("expanded");
|
||||||
|
toggleContents.style.height = "0px";
|
||||||
|
tocShowing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get rid of any expanded toggle if the user scrolls
|
||||||
|
window.document.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
throttle(() => {
|
||||||
|
closeToggle();
|
||||||
|
}, 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle positioning of the toggle
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
throttle(() => {
|
||||||
|
elRect = undefined;
|
||||||
|
positionToggle();
|
||||||
|
}, 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener("quarto-hrChanged", () => {
|
||||||
|
elRect = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process the click
|
||||||
|
clickEl.onclick = () => {
|
||||||
|
if (!tocShowing) {
|
||||||
|
toggleContainer.classList.add("expanded");
|
||||||
|
toggleContents.style.height = null;
|
||||||
|
tocShowing = true;
|
||||||
|
} else {
|
||||||
|
closeToggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Converts a sidebar from a menu back to a sidebar
|
||||||
|
const convertToSidebar = () => {
|
||||||
|
for (const child of el.children) {
|
||||||
|
child.style.opacity = 1;
|
||||||
|
child.style.overflow = null;
|
||||||
|
child.style.pointerEvents = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderEl = window.document.getElementById(
|
||||||
|
placeholderDescriptor.id
|
||||||
|
);
|
||||||
|
if (placeholderEl) {
|
||||||
|
placeholderEl.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
el.classList.remove("rollup");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isReaderMode()) {
|
||||||
|
convertToMenu();
|
||||||
|
isVisible = false;
|
||||||
|
} else {
|
||||||
|
// Find the top and bottom o the element that is being managed
|
||||||
|
const elTop = el.offsetTop;
|
||||||
|
const elBottom =
|
||||||
|
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
// If the element is current not visible reveal if there are
|
||||||
|
// no conflicts with overlay regions
|
||||||
|
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||||
|
convertToSidebar();
|
||||||
|
isVisible = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the element is visible, hide it if it conflicts with overlay regions
|
||||||
|
// and insert a placeholder toggle (or if we're in reader mode)
|
||||||
|
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||||
|
convertToMenu();
|
||||||
|
isVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||||
|
for (const tabEl of tabEls) {
|
||||||
|
const id = tabEl.getAttribute("data-bs-target");
|
||||||
|
if (id) {
|
||||||
|
const columnEl = document.querySelector(
|
||||||
|
`${id} .column-margin, .tabset-margin-content`
|
||||||
|
);
|
||||||
|
if (columnEl)
|
||||||
|
tabEl.addEventListener("shown.bs.tab", function (event) {
|
||||||
|
const el = event.srcElement;
|
||||||
|
if (el) {
|
||||||
|
const visibleCls = `${el.id}-margin-content`;
|
||||||
|
// walk up until we find a parent tabset
|
||||||
|
let panelTabsetEl = el.parentElement;
|
||||||
|
while (panelTabsetEl) {
|
||||||
|
if (panelTabsetEl.classList.contains("panel-tabset")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
panelTabsetEl = panelTabsetEl.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panelTabsetEl) {
|
||||||
|
const prevSib = panelTabsetEl.previousElementSibling;
|
||||||
|
if (
|
||||||
|
prevSib &&
|
||||||
|
prevSib.classList.contains("tabset-margin-container")
|
||||||
|
) {
|
||||||
|
const childNodes = prevSib.querySelectorAll(
|
||||||
|
".tabset-margin-content"
|
||||||
|
);
|
||||||
|
for (const childEl of childNodes) {
|
||||||
|
if (childEl.classList.contains(visibleCls)) {
|
||||||
|
childEl.classList.remove("collapse");
|
||||||
|
} else {
|
||||||
|
childEl.classList.add("collapse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutMarginEls();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage the visibility of the toc and the sidebar
|
||||||
|
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
|
||||||
|
id: "quarto-toc-toggle",
|
||||||
|
titleSelector: "#toc-title",
|
||||||
|
dismissOnClick: true,
|
||||||
|
});
|
||||||
|
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
|
||||||
|
id: "quarto-sidebarnav-toggle",
|
||||||
|
titleSelector: ".title",
|
||||||
|
dismissOnClick: false,
|
||||||
|
});
|
||||||
|
let tocLeftScrollVisibility;
|
||||||
|
if (leftTocEl) {
|
||||||
|
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
|
||||||
|
id: "quarto-lefttoc-toggle",
|
||||||
|
titleSelector: "#toc-title",
|
||||||
|
dismissOnClick: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first element that uses formatting in special columns
|
||||||
|
const conflictingEls = window.document.body.querySelectorAll(
|
||||||
|
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter all the possibly conflicting elements into ones
|
||||||
|
// the do conflict on the left or ride side
|
||||||
|
const arrConflictingEls = Array.from(conflictingEls);
|
||||||
|
const leftSideConflictEls = arrConflictingEls.filter((el) => {
|
||||||
|
if (el.tagName === "ASIDE") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Array.from(el.classList).find((className) => {
|
||||||
|
return (
|
||||||
|
className !== "column-body" &&
|
||||||
|
className.startsWith("column-") &&
|
||||||
|
!className.endsWith("right") &&
|
||||||
|
!className.endsWith("container") &&
|
||||||
|
className !== "column-margin"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const rightSideConflictEls = arrConflictingEls.filter((el) => {
|
||||||
|
if (el.tagName === "ASIDE") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMarginCaption = Array.from(el.classList).find((className) => {
|
||||||
|
return className == "margin-caption";
|
||||||
|
});
|
||||||
|
if (hasMarginCaption) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(el.classList).find((className) => {
|
||||||
|
return (
|
||||||
|
className !== "column-body" &&
|
||||||
|
!className.endsWith("container") &&
|
||||||
|
className.startsWith("column-") &&
|
||||||
|
!className.endsWith("left")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const kOverlapPaddingSize = 10;
|
||||||
|
function toRegions(els) {
|
||||||
|
return els.map((el) => {
|
||||||
|
const boundRect = el.getBoundingClientRect();
|
||||||
|
const top =
|
||||||
|
boundRect.top +
|
||||||
|
document.documentElement.scrollTop -
|
||||||
|
kOverlapPaddingSize;
|
||||||
|
return {
|
||||||
|
top,
|
||||||
|
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasObserved = false;
|
||||||
|
const visibleItemObserver = (els) => {
|
||||||
|
let visibleElements = [...els];
|
||||||
|
const intersectionObserver = new IntersectionObserver(
|
||||||
|
(entries, _observer) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (visibleElements.indexOf(entry.target) === -1) {
|
||||||
|
visibleElements.push(entry.target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
visibleElements = visibleElements.filter((visibleEntry) => {
|
||||||
|
return visibleEntry !== entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasObserved) {
|
||||||
|
hideOverlappedSidebars();
|
||||||
|
}
|
||||||
|
hasObserved = true;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
els.forEach((el) => {
|
||||||
|
intersectionObserver.observe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getVisibleEntries: () => {
|
||||||
|
return visibleElements;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
|
||||||
|
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
|
||||||
|
|
||||||
|
const hideOverlappedSidebars = () => {
|
||||||
|
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
|
||||||
|
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
|
||||||
|
if (tocLeftScrollVisibility) {
|
||||||
|
tocLeftScrollVisibility(
|
||||||
|
toRegions(leftElementObserver.getVisibleEntries())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.quartoToggleReader = () => {
|
||||||
|
// Applies a slow class (or removes it)
|
||||||
|
// to update the transition speed
|
||||||
|
const slowTransition = (slow) => {
|
||||||
|
const manageTransition = (id, slow) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
if (slow) {
|
||||||
|
el.classList.add("slow");
|
||||||
|
} else {
|
||||||
|
el.classList.remove("slow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
manageTransition("TOC", slow);
|
||||||
|
manageTransition("quarto-sidebar", slow);
|
||||||
|
};
|
||||||
|
const readerMode = !isReaderMode();
|
||||||
|
setReaderModeValue(readerMode);
|
||||||
|
|
||||||
|
// If we're entering reader mode, slow the transition
|
||||||
|
if (readerMode) {
|
||||||
|
slowTransition(readerMode);
|
||||||
|
}
|
||||||
|
highlightReaderToggle(readerMode);
|
||||||
|
hideOverlappedSidebars();
|
||||||
|
|
||||||
|
// If we're exiting reader mode, restore the non-slow transition
|
||||||
|
if (!readerMode) {
|
||||||
|
slowTransition(!readerMode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightReaderToggle = (readerMode) => {
|
||||||
|
const els = document.querySelectorAll(".quarto-reader-toggle");
|
||||||
|
if (els) {
|
||||||
|
els.forEach((el) => {
|
||||||
|
if (readerMode) {
|
||||||
|
el.classList.add("reader");
|
||||||
|
} else {
|
||||||
|
el.classList.remove("reader");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setReaderModeValue = (val) => {
|
||||||
|
if (window.location.protocol !== "file:") {
|
||||||
|
window.localStorage.setItem("quarto-reader-mode", val);
|
||||||
|
} else {
|
||||||
|
localReaderMode = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReaderMode = () => {
|
||||||
|
if (window.location.protocol !== "file:") {
|
||||||
|
return window.localStorage.getItem("quarto-reader-mode") === "true";
|
||||||
|
} else {
|
||||||
|
return localReaderMode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let localReaderMode = null;
|
||||||
|
|
||||||
|
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
|
||||||
|
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
|
||||||
|
|
||||||
|
// Walk the TOC and collapse/expand nodes
|
||||||
|
// Nodes are expanded if:
|
||||||
|
// - they are top level
|
||||||
|
// - they have children that are 'active' links
|
||||||
|
// - they are directly below an link that is 'active'
|
||||||
|
const walk = (el, depth) => {
|
||||||
|
// Tick depth when we enter a UL
|
||||||
|
if (el.tagName === "UL") {
|
||||||
|
depth = depth + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It this is active link
|
||||||
|
let isActiveNode = false;
|
||||||
|
if (el.tagName === "A" && el.classList.contains("active")) {
|
||||||
|
isActiveNode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if there is an active child to this element
|
||||||
|
let hasActiveChild = false;
|
||||||
|
for (const child of el.children) {
|
||||||
|
hasActiveChild = walk(child, depth) || hasActiveChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the collapse state if this is an UL
|
||||||
|
if (el.tagName === "UL") {
|
||||||
|
if (tocOpenDepth === -1 && depth > 1) {
|
||||||
|
// toc-expand: false
|
||||||
|
el.classList.add("collapse");
|
||||||
|
} else if (
|
||||||
|
depth <= tocOpenDepth ||
|
||||||
|
hasActiveChild ||
|
||||||
|
prevSiblingIsActiveLink(el)
|
||||||
|
) {
|
||||||
|
el.classList.remove("collapse");
|
||||||
|
} else {
|
||||||
|
el.classList.add("collapse");
|
||||||
|
}
|
||||||
|
|
||||||
|
// untick depth when we leave a UL
|
||||||
|
depth = depth - 1;
|
||||||
|
}
|
||||||
|
return hasActiveChild || isActiveNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// walk the TOC and expand / collapse any items that should be shown
|
||||||
|
if (tocEl) {
|
||||||
|
updateActiveLink();
|
||||||
|
walk(tocEl, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle the scroll event and walk peridiocally
|
||||||
|
window.document.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
throttle(() => {
|
||||||
|
if (tocEl) {
|
||||||
|
updateActiveLink();
|
||||||
|
walk(tocEl, 0);
|
||||||
|
}
|
||||||
|
if (!isReaderMode()) {
|
||||||
|
hideOverlappedSidebars();
|
||||||
|
}
|
||||||
|
}, 5)
|
||||||
|
);
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
throttle(() => {
|
||||||
|
if (tocEl) {
|
||||||
|
updateActiveLink();
|
||||||
|
walk(tocEl, 0);
|
||||||
|
}
|
||||||
|
if (!isReaderMode()) {
|
||||||
|
hideOverlappedSidebars();
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
);
|
||||||
|
hideOverlappedSidebars();
|
||||||
|
highlightReaderToggle(isReaderMode());
|
||||||
|
});
|
||||||
|
|
||||||
|
tabsets.init();
|
||||||
|
|
||||||
|
function throttle(func, wait) {
|
||||||
|
let waiting = false;
|
||||||
|
return function () {
|
||||||
|
if (!waiting) {
|
||||||
|
func.apply(this, arguments);
|
||||||
|
waiting = true;
|
||||||
|
setTimeout(function () {
|
||||||
|
waiting = false;
|
||||||
|
}, wait);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nexttick(func) {
|
||||||
|
return setTimeout(func, 0);
|
||||||
|
}
|
95
dist/site_libs/quarto-html/tabsets/tabsets.js
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// grouped tabsets
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
window.addEventListener("pageshow", (_event) => {
|
||||||
|
function getTabSettings() {
|
||||||
|
const data = localStorage.getItem("quarto-persistent-tabsets-data");
|
||||||
|
if (!data) {
|
||||||
|
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTabSettings(data) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"quarto-persistent-tabsets-data",
|
||||||
|
JSON.stringify(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTabState(groupName, groupValue) {
|
||||||
|
const data = getTabSettings();
|
||||||
|
data[groupName] = groupValue;
|
||||||
|
setTabSettings(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTab(tab, active) {
|
||||||
|
const tabPanelId = tab.getAttribute("aria-controls");
|
||||||
|
const tabPanel = document.getElementById(tabPanelId);
|
||||||
|
if (active) {
|
||||||
|
tab.classList.add("active");
|
||||||
|
tabPanel.classList.add("active");
|
||||||
|
} else {
|
||||||
|
tab.classList.remove("active");
|
||||||
|
tabPanel.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(selectedGroup, selectorsToSync) {
|
||||||
|
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
|
||||||
|
const active = selectedGroup === thisGroup;
|
||||||
|
for (const tab of tabs) {
|
||||||
|
toggleTab(tab, active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSelectorsToSyncByLanguage() {
|
||||||
|
const result = {};
|
||||||
|
const tabs = Array.from(
|
||||||
|
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
|
||||||
|
);
|
||||||
|
for (const item of tabs) {
|
||||||
|
const div = item.parentElement.parentElement.parentElement;
|
||||||
|
const group = div.getAttribute("data-group");
|
||||||
|
if (!result[group]) {
|
||||||
|
result[group] = {};
|
||||||
|
}
|
||||||
|
const selectorsToSync = result[group];
|
||||||
|
const value = item.innerHTML;
|
||||||
|
if (!selectorsToSync[value]) {
|
||||||
|
selectorsToSync[value] = [];
|
||||||
|
}
|
||||||
|
selectorsToSync[value].push(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSelectorSync() {
|
||||||
|
const selectorsToSync = findSelectorsToSyncByLanguage();
|
||||||
|
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
|
||||||
|
Object.entries(tabSetsByValue).forEach(([value, items]) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
item.addEventListener("click", (_event) => {
|
||||||
|
setTabState(group, value);
|
||||||
|
toggleAll(value, selectorsToSync[group]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return selectorsToSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectorsToSync = setupSelectorSync();
|
||||||
|
for (const [group, selectedName] of Object.entries(getTabSettings())) {
|
||||||
|
const selectors = selectorsToSync[group];
|
||||||
|
// it's possible that stale state gives us empty selections, so we explicitly check here.
|
||||||
|
if (selectors) {
|
||||||
|
toggleAll(selectedName, selectors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
1
dist/site_libs/quarto-html/tippy.css
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
|
2
dist/site_libs/quarto-html/tippy.umd.min.js
vendored
Normal file
2
dist/site_libs/quarto-listing/list.min.js
vendored
Normal file
254
dist/site_libs/quarto-listing/quarto-listing.js
vendored
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
const kProgressiveAttr = "data-src";
|
||||||
|
let categoriesLoaded = false;
|
||||||
|
|
||||||
|
window.quartoListingCategory = (category) => {
|
||||||
|
// category is URI encoded in EJS template for UTF-8 support
|
||||||
|
category = decodeURIComponent(atob(category));
|
||||||
|
if (categoriesLoaded) {
|
||||||
|
activateCategory(category);
|
||||||
|
setCategoryHash(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window["quarto-listing-loaded"] = () => {
|
||||||
|
// Process any existing hash
|
||||||
|
const hash = getHash();
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
// If there is a category, switch to that
|
||||||
|
if (hash.category) {
|
||||||
|
// category hash are URI encoded so we need to decode it before processing
|
||||||
|
// so that we can match it with the category element processed in JS
|
||||||
|
activateCategory(decodeURIComponent(hash.category));
|
||||||
|
}
|
||||||
|
// Paginate a specific listing
|
||||||
|
const listingIds = Object.keys(window["quarto-listings"]);
|
||||||
|
for (const listingId of listingIds) {
|
||||||
|
const page = hash[getListingPageKey(listingId)];
|
||||||
|
if (page) {
|
||||||
|
showPage(listingId, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listingIds = Object.keys(window["quarto-listings"]);
|
||||||
|
for (const listingId of listingIds) {
|
||||||
|
// The actual list
|
||||||
|
const list = window["quarto-listings"][listingId];
|
||||||
|
|
||||||
|
// Update the handlers for pagination events
|
||||||
|
refreshPaginationHandlers(listingId);
|
||||||
|
|
||||||
|
// Render any visible items that need it
|
||||||
|
renderVisibleProgressiveImages(list);
|
||||||
|
|
||||||
|
// Whenever the list is updated, we also need to
|
||||||
|
// attach handlers to the new pagination elements
|
||||||
|
// and refresh any newly visible items.
|
||||||
|
list.on("updated", function () {
|
||||||
|
renderVisibleProgressiveImages(list);
|
||||||
|
setTimeout(() => refreshPaginationHandlers(listingId));
|
||||||
|
|
||||||
|
// Show or hide the no matching message
|
||||||
|
toggleNoMatchingMessage(list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.document.addEventListener("DOMContentLoaded", function (_event) {
|
||||||
|
// Attach click handlers to categories
|
||||||
|
const categoryEls = window.document.querySelectorAll(
|
||||||
|
".quarto-listing-category .category"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const categoryEl of categoryEls) {
|
||||||
|
// category needs to support non ASCII characters
|
||||||
|
const category = decodeURIComponent(
|
||||||
|
atob(categoryEl.getAttribute("data-category"))
|
||||||
|
);
|
||||||
|
categoryEl.onclick = () => {
|
||||||
|
activateCategory(category);
|
||||||
|
setCategoryHash(category);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach a click handler to the category title
|
||||||
|
// (there should be only one, but since it is a class name, handle N)
|
||||||
|
const categoryTitleEls = window.document.querySelectorAll(
|
||||||
|
".quarto-listing-category-title"
|
||||||
|
);
|
||||||
|
for (const categoryTitleEl of categoryTitleEls) {
|
||||||
|
categoryTitleEl.onclick = () => {
|
||||||
|
activateCategory("");
|
||||||
|
setCategoryHash("");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
categoriesLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleNoMatchingMessage(list) {
|
||||||
|
const selector = `#${list.listContainer.id} .listing-no-matching`;
|
||||||
|
const noMatchingEl = window.document.querySelector(selector);
|
||||||
|
if (noMatchingEl) {
|
||||||
|
if (list.visibleItems.length === 0) {
|
||||||
|
noMatchingEl.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
if (!noMatchingEl.classList.contains("d-none")) {
|
||||||
|
noMatchingEl.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCategoryHash(category) {
|
||||||
|
setHash({ category });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageHash(listingId, page) {
|
||||||
|
const currentHash = getHash() || {};
|
||||||
|
currentHash[getListingPageKey(listingId)] = page;
|
||||||
|
setHash(currentHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListingPageKey(listingId) {
|
||||||
|
return `${listingId}-page`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPaginationHandlers(listingId) {
|
||||||
|
const listingEl = window.document.getElementById(listingId);
|
||||||
|
const paginationEls = listingEl.querySelectorAll(
|
||||||
|
".pagination li.page-item:not(.disabled) .page.page-link"
|
||||||
|
);
|
||||||
|
for (const paginationEl of paginationEls) {
|
||||||
|
paginationEl.onclick = (sender) => {
|
||||||
|
setPageHash(listingId, sender.target.getAttribute("data-i"));
|
||||||
|
showPage(listingId, sender.target.getAttribute("data-i"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVisibleProgressiveImages(list) {
|
||||||
|
// Run through the visible items and render any progressive images
|
||||||
|
for (const item of list.visibleItems) {
|
||||||
|
const itemEl = item.elm;
|
||||||
|
if (itemEl) {
|
||||||
|
const progressiveImgs = itemEl.querySelectorAll(
|
||||||
|
`img[${kProgressiveAttr}]`
|
||||||
|
);
|
||||||
|
for (const progressiveImg of progressiveImgs) {
|
||||||
|
const srcValue = progressiveImg.getAttribute(kProgressiveAttr);
|
||||||
|
if (srcValue) {
|
||||||
|
progressiveImg.setAttribute("src", srcValue);
|
||||||
|
}
|
||||||
|
progressiveImg.removeAttribute(kProgressiveAttr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHash() {
|
||||||
|
// Hashes are of the form
|
||||||
|
// #name:value|name1:value1|name2:value2
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined;
|
||||||
|
return parseHash(hashRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kAnd = "&";
|
||||||
|
const kEquals = "=";
|
||||||
|
|
||||||
|
function parseHash(hash) {
|
||||||
|
if (!hash) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const hasValuesStrs = hash.split(kAnd);
|
||||||
|
const hashValues = hasValuesStrs
|
||||||
|
.map((hashValueStr) => {
|
||||||
|
const vals = hashValueStr.split(kEquals);
|
||||||
|
if (vals.length === 2) {
|
||||||
|
return { name: vals[0], value: vals[1] };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((value) => {
|
||||||
|
return value !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hashObj = {};
|
||||||
|
hashValues.forEach((hashValue) => {
|
||||||
|
hashObj[hashValue.name] = decodeURIComponent(hashValue.value);
|
||||||
|
});
|
||||||
|
return hashObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHash(obj) {
|
||||||
|
return Object.keys(obj)
|
||||||
|
.map((key) => {
|
||||||
|
return `${key}${kEquals}${obj[key]}`;
|
||||||
|
})
|
||||||
|
.join(kAnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHash(obj) {
|
||||||
|
const hash = makeHash(obj);
|
||||||
|
window.history.pushState(null, null, `#${hash}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPage(listingId, page) {
|
||||||
|
const list = window["quarto-listings"][listingId];
|
||||||
|
if (list) {
|
||||||
|
list.show((page - 1) * list.page + 1, list.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateCategory(category) {
|
||||||
|
// Deactivate existing categories
|
||||||
|
const activeEls = window.document.querySelectorAll(
|
||||||
|
".quarto-listing-category .category.active"
|
||||||
|
);
|
||||||
|
for (const activeEl of activeEls) {
|
||||||
|
activeEl.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate this category
|
||||||
|
const categoryEl = window.document.querySelector(
|
||||||
|
`.quarto-listing-category .category[data-category='${btoa(
|
||||||
|
encodeURIComponent(category)
|
||||||
|
)}']`
|
||||||
|
);
|
||||||
|
if (categoryEl) {
|
||||||
|
categoryEl.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the listings to this category
|
||||||
|
filterListingCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListingCategory(category) {
|
||||||
|
const listingIds = Object.keys(window["quarto-listings"]);
|
||||||
|
for (const listingId of listingIds) {
|
||||||
|
const list = window["quarto-listings"][listingId];
|
||||||
|
if (list) {
|
||||||
|
if (category === "") {
|
||||||
|
// resets the filter
|
||||||
|
list.filter();
|
||||||
|
} else {
|
||||||
|
// filter to this category
|
||||||
|
list.filter(function (item) {
|
||||||
|
const itemValues = item.values();
|
||||||
|
if (itemValues.categories !== null) {
|
||||||
|
const categories = decodeURIComponent(
|
||||||
|
atob(itemValues.categories)
|
||||||
|
).split(",");
|
||||||
|
return categories.includes(category);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
dist/site_libs/quarto-nav/headroom.min.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/*!
|
||||||
|
* headroom.js v0.12.0 - Give your page some headroom. Hide your header until you need it
|
||||||
|
* Copyright (c) 2020 Nick Williams - http://wicky.nillia.ms/headroom.js
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).Headroom=n()}(this,function(){"use strict";function t(){return"undefined"!=typeof window}function d(t){return function(t){return t&&t.document&&function(t){return 9===t.nodeType}(t.document)}(t)?function(t){var n=t.document,o=n.body,s=n.documentElement;return{scrollHeight:function(){return Math.max(o.scrollHeight,s.scrollHeight,o.offsetHeight,s.offsetHeight,o.clientHeight,s.clientHeight)},height:function(){return t.innerHeight||s.clientHeight||o.clientHeight},scrollY:function(){return void 0!==t.pageYOffset?t.pageYOffset:(s||o.parentNode||o).scrollTop}}}(t):function(t){return{scrollHeight:function(){return Math.max(t.scrollHeight,t.offsetHeight,t.clientHeight)},height:function(){return Math.max(t.offsetHeight,t.clientHeight)},scrollY:function(){return t.scrollTop}}}(t)}function n(t,s,e){var n,o=function(){var n=!1;try{var t={get passive(){n=!0}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){n=!1}return n}(),i=!1,r=d(t),l=r.scrollY(),a={};function c(){var t=Math.round(r.scrollY()),n=r.height(),o=r.scrollHeight();a.scrollY=t,a.lastScrollY=l,a.direction=l<t?"down":"up",a.distance=Math.abs(t-l),a.isOutOfBounds=t<0||o<t+n,a.top=t<=s.offset[a.direction],a.bottom=o<=t+n,a.toleranceExceeded=a.distance>s.tolerance[a.direction],e(a),l=t,i=!1}function h(){i||(i=!0,n=requestAnimationFrame(c))}var u=!!o&&{passive:!0,capture:!1};return t.addEventListener("scroll",h,u),c(),{destroy:function(){cancelAnimationFrame(n),t.removeEventListener("scroll",h,u)}}}function o(t){return t===Object(t)?t:{down:t,up:t}}function s(t,n){n=n||{},Object.assign(this,s.options,n),this.classes=Object.assign({},s.options.classes,n.classes),this.elem=t,this.tolerance=o(this.tolerance),this.offset=o(this.offset),this.initialised=!1,this.frozen=!1}return s.prototype={constructor:s,init:function(){return s.cutsTheMustard&&!this.initialised&&(this.addClass("initial"),this.initialised=!0,setTimeout(function(t){t.scrollTracker=n(t.scroller,{offset:t.offset,tolerance:t.tolerance},t.update.bind(t))},100,this)),this},destroy:function(){this.initialised=!1,Object.keys(this.classes).forEach(this.removeClass,this),this.scrollTracker.destroy()},unpin:function(){!this.hasClass("pinned")&&this.hasClass("unpinned")||(this.addClass("unpinned"),this.removeClass("pinned"),this.onUnpin&&this.onUnpin.call(this))},pin:function(){this.hasClass("unpinned")&&(this.addClass("pinned"),this.removeClass("unpinned"),this.onPin&&this.onPin.call(this))},freeze:function(){this.frozen=!0,this.addClass("frozen")},unfreeze:function(){this.frozen=!1,this.removeClass("frozen")},top:function(){this.hasClass("top")||(this.addClass("top"),this.removeClass("notTop"),this.onTop&&this.onTop.call(this))},notTop:function(){this.hasClass("notTop")||(this.addClass("notTop"),this.removeClass("top"),this.onNotTop&&this.onNotTop.call(this))},bottom:function(){this.hasClass("bottom")||(this.addClass("bottom"),this.removeClass("notBottom"),this.onBottom&&this.onBottom.call(this))},notBottom:function(){this.hasClass("notBottom")||(this.addClass("notBottom"),this.removeClass("bottom"),this.onNotBottom&&this.onNotBottom.call(this))},shouldUnpin:function(t){return"down"===t.direction&&!t.top&&t.toleranceExceeded},shouldPin:function(t){return"up"===t.direction&&t.toleranceExceeded||t.top},addClass:function(t){this.elem.classList.add.apply(this.elem.classList,this.classes[t].split(" "))},removeClass:function(t){this.elem.classList.remove.apply(this.elem.classList,this.classes[t].split(" "))},hasClass:function(t){return this.classes[t].split(" ").every(function(t){return this.classList.contains(t)},this.elem)},update:function(t){t.isOutOfBounds||!0!==this.frozen&&(t.top?this.top():this.notTop(),t.bottom?this.bottom():this.notBottom(),this.shouldUnpin(t)?this.unpin():this.shouldPin(t)&&this.pin())}},s.options={tolerance:{up:0,down:0},offset:0,scroller:t()?window:null,classes:{frozen:"headroom--frozen",pinned:"headroom--pinned",unpinned:"headroom--unpinned",top:"headroom--top",notTop:"headroom--not-top",bottom:"headroom--bottom",notBottom:"headroom--not-bottom",initial:"headroom"}},s.cutsTheMustard=!!(t()&&function(){}.bind&&"classList"in document.documentElement&&Object.assign&&Object.keys&&requestAnimationFrame),s});
|
325
dist/site_libs/quarto-nav/quarto-nav.js
vendored
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
const headroomChanged = new CustomEvent("quarto-hrChanged", {
|
||||||
|
detail: {},
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
composed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const announceDismiss = () => {
|
||||||
|
const annEl = window.document.getElementById("quarto-announcement");
|
||||||
|
if (annEl) {
|
||||||
|
annEl.remove();
|
||||||
|
|
||||||
|
const annId = annEl.getAttribute("data-announcement-id");
|
||||||
|
window.localStorage.setItem(`quarto-announce-${annId}`, "true");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const announceRegister = () => {
|
||||||
|
const annEl = window.document.getElementById("quarto-announcement");
|
||||||
|
if (annEl) {
|
||||||
|
const annId = annEl.getAttribute("data-announcement-id");
|
||||||
|
const isDismissed =
|
||||||
|
window.localStorage.getItem(`quarto-announce-${annId}`) || false;
|
||||||
|
if (isDismissed) {
|
||||||
|
announceDismiss();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
annEl.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionEl = annEl.querySelector(".quarto-announcement-action");
|
||||||
|
if (actionEl) {
|
||||||
|
actionEl.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Hide the bar immediately
|
||||||
|
announceDismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
let init = false;
|
||||||
|
|
||||||
|
announceRegister();
|
||||||
|
|
||||||
|
// Manage the back to top button, if one is present.
|
||||||
|
let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
const scrollDownBuffer = 5;
|
||||||
|
const scrollUpBuffer = 35;
|
||||||
|
const btn = document.getElementById("quarto-back-to-top");
|
||||||
|
const hideBackToTop = () => {
|
||||||
|
btn.style.display = "none";
|
||||||
|
};
|
||||||
|
const showBackToTop = () => {
|
||||||
|
btn.style.display = "inline-block";
|
||||||
|
};
|
||||||
|
if (btn) {
|
||||||
|
window.document.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
function () {
|
||||||
|
const currentScrollTop =
|
||||||
|
window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
// Shows and hides the button 'intelligently' as the user scrolls
|
||||||
|
if (currentScrollTop - scrollDownBuffer > lastScrollTop) {
|
||||||
|
hideBackToTop();
|
||||||
|
lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop;
|
||||||
|
} else if (currentScrollTop < lastScrollTop - scrollUpBuffer) {
|
||||||
|
showBackToTop();
|
||||||
|
lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the button at the bottom, hides it at the top
|
||||||
|
if (currentScrollTop <= 0) {
|
||||||
|
hideBackToTop();
|
||||||
|
} else if (
|
||||||
|
window.innerHeight + currentScrollTop >=
|
||||||
|
document.body.offsetHeight
|
||||||
|
) {
|
||||||
|
showBackToTop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function throttle(func, wait) {
|
||||||
|
var timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
const later = function () {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
func.apply(context, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!timeout) {
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerOffset() {
|
||||||
|
// Set an offset if there is are fixed top navbar
|
||||||
|
const headerEl = window.document.querySelector("header.fixed-top");
|
||||||
|
if (headerEl) {
|
||||||
|
return headerEl.clientHeight;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function footerOffset() {
|
||||||
|
const footerEl = window.document.querySelector("footer.footer");
|
||||||
|
if (footerEl) {
|
||||||
|
return footerEl.clientHeight;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboardOffset() {
|
||||||
|
const dashboardNavEl = window.document.getElementById(
|
||||||
|
"quarto-dashboard-header"
|
||||||
|
);
|
||||||
|
if (dashboardNavEl !== null) {
|
||||||
|
return dashboardNavEl.clientHeight;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDocumentOffsetWithoutAnimation() {
|
||||||
|
updateDocumentOffset(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDocumentOffset(animated) {
|
||||||
|
// set body offset
|
||||||
|
const topOffset = headerOffset();
|
||||||
|
const bodyOffset = topOffset + footerOffset() + dashboardOffset();
|
||||||
|
const bodyEl = window.document.body;
|
||||||
|
bodyEl.setAttribute("data-bs-offset", topOffset);
|
||||||
|
bodyEl.style.paddingTop = topOffset + "px";
|
||||||
|
|
||||||
|
// deal with sidebar offsets
|
||||||
|
const sidebars = window.document.querySelectorAll(
|
||||||
|
".sidebar, .headroom-target"
|
||||||
|
);
|
||||||
|
sidebars.forEach((sidebar) => {
|
||||||
|
if (!animated) {
|
||||||
|
sidebar.classList.add("notransition");
|
||||||
|
// Remove the no transition class after the animation has time to complete
|
||||||
|
setTimeout(function () {
|
||||||
|
sidebar.classList.remove("notransition");
|
||||||
|
}, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) {
|
||||||
|
sidebar.style.top = "0";
|
||||||
|
sidebar.style.maxHeight = "100vh";
|
||||||
|
} else {
|
||||||
|
sidebar.style.top = topOffset + "px";
|
||||||
|
sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// allow space for footer
|
||||||
|
const mainContainer = window.document.querySelector(".quarto-container");
|
||||||
|
if (mainContainer) {
|
||||||
|
mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// link offset
|
||||||
|
let linkStyle = window.document.querySelector("#quarto-target-style");
|
||||||
|
if (!linkStyle) {
|
||||||
|
linkStyle = window.document.createElement("style");
|
||||||
|
linkStyle.setAttribute("id", "quarto-target-style");
|
||||||
|
window.document.head.appendChild(linkStyle);
|
||||||
|
}
|
||||||
|
while (linkStyle.firstChild) {
|
||||||
|
linkStyle.removeChild(linkStyle.firstChild);
|
||||||
|
}
|
||||||
|
if (topOffset > 0) {
|
||||||
|
linkStyle.appendChild(
|
||||||
|
window.document.createTextNode(`
|
||||||
|
section:target::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: ${topOffset}px;
|
||||||
|
margin: -${topOffset}px 0 0;
|
||||||
|
}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (init) {
|
||||||
|
window.dispatchEvent(headroomChanged);
|
||||||
|
}
|
||||||
|
init = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize headroom
|
||||||
|
var header = window.document.querySelector("#quarto-header");
|
||||||
|
if (header && window.Headroom) {
|
||||||
|
const headroom = new window.Headroom(header, {
|
||||||
|
tolerance: 5,
|
||||||
|
onPin: function () {
|
||||||
|
const sidebars = window.document.querySelectorAll(
|
||||||
|
".sidebar, .headroom-target"
|
||||||
|
);
|
||||||
|
sidebars.forEach((sidebar) => {
|
||||||
|
sidebar.classList.remove("sidebar-unpinned");
|
||||||
|
});
|
||||||
|
updateDocumentOffset();
|
||||||
|
},
|
||||||
|
onUnpin: function () {
|
||||||
|
const sidebars = window.document.querySelectorAll(
|
||||||
|
".sidebar, .headroom-target"
|
||||||
|
);
|
||||||
|
sidebars.forEach((sidebar) => {
|
||||||
|
sidebar.classList.add("sidebar-unpinned");
|
||||||
|
});
|
||||||
|
updateDocumentOffset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
headroom.init();
|
||||||
|
|
||||||
|
let frozen = false;
|
||||||
|
window.quartoToggleHeadroom = function () {
|
||||||
|
if (frozen) {
|
||||||
|
headroom.unfreeze();
|
||||||
|
frozen = false;
|
||||||
|
} else {
|
||||||
|
headroom.freeze();
|
||||||
|
frozen = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"hashchange",
|
||||||
|
function (e) {
|
||||||
|
if (
|
||||||
|
getComputedStyle(document.documentElement).scrollBehavior !== "smooth"
|
||||||
|
) {
|
||||||
|
window.scrollTo(0, window.pageYOffset - headerOffset());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observe size changed for the header
|
||||||
|
const headerEl = window.document.querySelector("header.fixed-top");
|
||||||
|
if (headerEl && window.ResizeObserver) {
|
||||||
|
const observer = new window.ResizeObserver(() => {
|
||||||
|
setTimeout(updateDocumentOffsetWithoutAnimation, 0);
|
||||||
|
});
|
||||||
|
observer.observe(headerEl, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
throttle(updateDocumentOffsetWithoutAnimation, 50)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setTimeout(updateDocumentOffsetWithoutAnimation, 250);
|
||||||
|
|
||||||
|
// fixup index.html links if we aren't on the filesystem
|
||||||
|
if (window.location.protocol !== "file:") {
|
||||||
|
const links = window.document.querySelectorAll("a");
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
if (links[i].href) {
|
||||||
|
links[i].dataset.originalHref = links[i].href;
|
||||||
|
links[i].href = links[i].href.replace(/\/index\.html/, "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixup any sharing links that require urls
|
||||||
|
// Append url to any sharing urls
|
||||||
|
const sharingLinks = window.document.querySelectorAll(
|
||||||
|
"a.sidebar-tools-main-item, a.quarto-navigation-tool, a.quarto-navbar-tools, a.quarto-navbar-tools-item"
|
||||||
|
);
|
||||||
|
for (let i = 0; i < sharingLinks.length; i++) {
|
||||||
|
const sharingLink = sharingLinks[i];
|
||||||
|
const href = sharingLink.getAttribute("href");
|
||||||
|
if (href) {
|
||||||
|
sharingLink.setAttribute(
|
||||||
|
"href",
|
||||||
|
href.replace("|url|", window.location.href)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll the active navigation item into view, if necessary
|
||||||
|
const navSidebar = window.document.querySelector("nav#quarto-sidebar");
|
||||||
|
if (navSidebar) {
|
||||||
|
// Find the active item
|
||||||
|
const activeItem = navSidebar.querySelector("li.sidebar-item a.active");
|
||||||
|
if (activeItem) {
|
||||||
|
// Wait for the scroll height and height to resolve by observing size changes on the
|
||||||
|
// nav element that is scrollable
|
||||||
|
const resizeObserver = new ResizeObserver((_entries) => {
|
||||||
|
// The bottom of the element
|
||||||
|
const elBottom = activeItem.offsetTop;
|
||||||
|
const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight;
|
||||||
|
|
||||||
|
// The element height and scroll height are the same, then we are still loading
|
||||||
|
if (viewBottom !== navSidebar.scrollHeight) {
|
||||||
|
// Determine if the item isn't visible and scroll to it
|
||||||
|
if (elBottom >= viewBottom) {
|
||||||
|
navSidebar.scrollTop = elBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop observing now since we've completed the scroll
|
||||||
|
resizeObserver.unobserve(navSidebar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(navSidebar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
3
dist/site_libs/quarto-search/autocomplete.umd.js
vendored
Normal file
9
dist/site_libs/quarto-search/fuse.min.js
vendored
Normal file
1290
dist/site_libs/quarto-search/quarto-search.js
vendored
Normal file
87
dist/sitemap.xml
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Writing/Obsidian-RAG.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:08:56.279Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Writing/documentation.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:09:26.317Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Uni/Lernerfolg_an_der_Uni.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:24:18.456Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Opinions/Keyboard-Layout.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:31:10.026Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Advantages.html</loc>
|
||||||
|
<lastmod>2025-05-09T07:11:06.603Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/FFPiH.html</loc>
|
||||||
|
<lastmod>2025-05-09T07:07:51.739Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Webapp-Example/MyService_Types.hs.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:05:11.127Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Code Snippets/Morphisms.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:06:01.303Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/About/index.html</loc>
|
||||||
|
<lastmod>2025-05-09T07:01:07.584Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/About/Work.html</loc>
|
||||||
|
<lastmod>2025-05-08T16:36:47.996Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/About/Experience.html</loc>
|
||||||
|
<lastmod>2025-05-08T16:36:47.996Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/About/Extracurricular.html</loc>
|
||||||
|
<lastmod>2025-05-08T16:36:48.003Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Code Snippets/Monoid.html</loc>
|
||||||
|
<lastmod>2025-05-09T07:18:58.729Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Webapp-Example/Main.hs.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:04:40.395Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Webapp-Example/index.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:20:12.478Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Coding/Haskell/Lenses.html</loc>
|
||||||
|
<lastmod>2025-05-09T07:10:26.159Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Health/Issues.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:29:56.847Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Opinions/Editors.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:29:47.541Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Stuff/Bielefeldverschwoerung.html</loc>
|
||||||
|
<lastmod>2025-05-09T18:31:48.525Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/index.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:33:02.487Z</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nicole.dresselhaus.cloud/Writing/ner4all-case-study.html</loc>
|
||||||
|
<lastmod>2025-05-09T19:07:58.041Z</lastmod>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
BIN
dist/thumbs/placeholder.png
vendored
Normal file
After Width: | Height: | Size: 380 KiB |
BIN
dist/thumbs/writing_documentation.png
vendored
Normal file
After Width: | Height: | Size: 269 KiB |
BIN
dist/thumbs/writing_ner4all-case-study.png
vendored
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
dist/thumbs/writing_obsidian-rag.png
vendored
Normal file
After Width: | Height: | Size: 322 KiB |
197
espresso.theme
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
{
|
||||||
|
"text-color": "#bdae9d",
|
||||||
|
"background-color": "#2a211c",
|
||||||
|
"line-number-color": "#bdae9d",
|
||||||
|
"line-number-background-color": "#2a211c",
|
||||||
|
"text-styles": {
|
||||||
|
"Alert": {
|
||||||
|
"text-color": "#ffff00",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Annotation": {
|
||||||
|
"text-color": "#0066ff",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": true,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Attribute": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"BaseN": {
|
||||||
|
"text-color": "#44aa43",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"BuiltIn": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Char": {
|
||||||
|
"text-color": "#049b0a",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Comment": {
|
||||||
|
"text-color": "#0066ff",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": true,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Constant": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"ControlFlow": {
|
||||||
|
"text-color": "#43a8ed",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"DataType": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": true
|
||||||
|
},
|
||||||
|
"DecVal": {
|
||||||
|
"text-color": "#44aa43",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Documentation": {
|
||||||
|
"text-color": "#0066ff",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": true,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Error": {
|
||||||
|
"text-color": "#ffff00",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Extension": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Float": {
|
||||||
|
"text-color": "#44aa43",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Function": {
|
||||||
|
"text-color": "#ff9358",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Import": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Information": {
|
||||||
|
"text-color": "#0066ff",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": true,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Keyword": {
|
||||||
|
"text-color": "#43a8ed",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Operator": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Preprocessor": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"SpecialChar": {
|
||||||
|
"text-color": "#049b0a",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"SpecialString": {
|
||||||
|
"text-color": "#049b0a",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"String": {
|
||||||
|
"text-color": "#049b0a",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Variable": {
|
||||||
|
"text-color": null,
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"VerbatimString": {
|
||||||
|
"text-color": "#049b0a",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
},
|
||||||
|
"Warning": {
|
||||||
|
"text-color": "#ffff00",
|
||||||
|
"background-color": null,
|
||||||
|
"bold": true,
|
||||||
|
"italic": false,
|
||||||
|
"underline": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
186
gruvbox-dark.theme
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
{
|
||||||
|
"_comments": [
|
||||||
|
"Last update: Sep 17, 2020 (revision 2)",
|
||||||
|
"This file has been converted from: https://github.com/morhetz/gruvbox"
|
||||||
|
],
|
||||||
|
"metadata" : {
|
||||||
|
"copyright": [
|
||||||
|
"SPDX-FileCopyrightText: 2017 Pavel Pertsev <morhetz@gmail.com>",
|
||||||
|
"SPDX-FileCopyrightText: 2020 Frederik Banning <laubblaeser@live.com>"
|
||||||
|
],
|
||||||
|
"license": "SPDX-License-Identifier: MIT",
|
||||||
|
"name" : "gruvbox Dark",
|
||||||
|
"revision" : 2
|
||||||
|
},
|
||||||
|
"text-styles": {
|
||||||
|
"Normal" : {
|
||||||
|
"text-color" : "#ebdbb2",
|
||||||
|
"selected-text-color" : "#ebdbb2",
|
||||||
|
"bold" : false,
|
||||||
|
"italic" : false,
|
||||||
|
"underline" : false,
|
||||||
|
"strike-through" : false
|
||||||
|
},
|
||||||
|
"Keyword" : {
|
||||||
|
"text-color" : "#ebdbb2",
|
||||||
|
"selected-text-color" : "#ebdbb2",
|
||||||
|
"bold" : true
|
||||||
|
},
|
||||||
|
"Function" : {
|
||||||
|
"text-color" : "#689d6a",
|
||||||
|
"selected-text-color" : "#8ec07c"
|
||||||
|
},
|
||||||
|
"Variable" : {
|
||||||
|
"text-color" : "#458588",
|
||||||
|
"selected-text-color" : "#83a598"
|
||||||
|
},
|
||||||
|
"ControlFlow" : {
|
||||||
|
"text-color" : "#cc241d",
|
||||||
|
"selected-text-color" : "#fb4934",
|
||||||
|
"bold" : true
|
||||||
|
},
|
||||||
|
"Operator" : {
|
||||||
|
"text-color" : "#ebdbb2",
|
||||||
|
"selected-text-color" : "#ebdbb2"
|
||||||
|
},
|
||||||
|
"BuiltIn" : {
|
||||||
|
"text-color" : "#d65d0e",
|
||||||
|
"selected-text-color" : "#fe8019"
|
||||||
|
},
|
||||||
|
"Extension" : {
|
||||||
|
"text-color" : "#689d6a",
|
||||||
|
"selected-text-color" : "#8ec07c",
|
||||||
|
"bold" : true
|
||||||
|
},
|
||||||
|
"Preprocessor" : {
|
||||||
|
"text-color" : "#d65d0e",
|
||||||
|
"selected-text-color" : "#fe8019"
|
||||||
|
},
|
||||||
|
"Attribute" : {
|
||||||
|
"text-color" : "#d79921",
|
||||||
|
"selected-text-color" : "#fabd2f"
|
||||||
|
},
|
||||||
|
"Char" : {
|
||||||
|
"text-color" : "#b16286",
|
||||||
|
"selected-text-color" : "#d3869b"
|
||||||
|
},
|
||||||
|
"SpecialChar" : {
|
||||||
|
"text-color" : "#b16286",
|
||||||
|
"selected-text-color" : "#d3869b"
|
||||||
|
},
|
||||||
|
"String" : {
|
||||||
|
"text-color" : "#98971a",
|
||||||
|
"selected-text-color" : "#b8bb26"
|
||||||
|
},
|
||||||
|
"VerbatimString" : {
|
||||||
|
"text-color" : "#98971a",
|
||||||
|
"selected-text-color" : "#b8bb26"
|
||||||
|
},
|
||||||
|
"SpecialString" : {
|
||||||
|
"text-color" : "#98971a",
|
||||||
|
"selected-text-color" : "#b8bb26"
|
||||||
|
},
|
||||||
|
"Import" : {
|
||||||
|
"text-color" : "#689d6a",
|
||||||
|
"selected-text-color" : "#8ec07c"
|
||||||
|
},
|
||||||
|
"DataType" : {
|
||||||
|
"text-color" : "#d79921",
|
||||||
|
"selected-text-color" : "#fabd2f"
|
||||||
|
},
|
||||||
|
"DecVal" : {
|
||||||
|
"text-color" : "#f67400",
|
||||||
|
"selected-text-color" : "#f67400"
|
||||||
|
},
|
||||||
|
"BaseN" : {
|
||||||
|
"text-color" : "#f67400",
|
||||||
|
"selected-text-color" : "#f67400"
|
||||||
|
},
|
||||||
|
"Float" : {
|
||||||
|
"text-color" : "#f67400",
|
||||||
|
"selected-text-color" : "#f67400"
|
||||||
|
},
|
||||||
|
"Constant" : {
|
||||||
|
"text-color" : "#b16286",
|
||||||
|
"selected-text-color" : "#d3869b",
|
||||||
|
"bold" : true
|
||||||
|
},
|
||||||
|
"Comment" : {
|
||||||
|
"text-color" : "#928374",
|
||||||
|
"selected-text-color" : "#a89984"
|
||||||
|
},
|
||||||
|
"Documentation" : {
|
||||||
|
"text-color" : "#98971a",
|
||||||
|
"selected-text-color" : "#b8bb26"
|
||||||
|
},
|
||||||
|
"Annotation" : {
|
||||||
|
"text-color" : "#98971a",
|
||||||
|
"selected-text-color" : "#b8bb26"
|
||||||
|
},
|
||||||
|
"CommentVar" : {
|
||||||
|
"text-color" : "#928374",
|
||||||
|
"selected-text-color" : "#a89984"
|
||||||
|
},
|
||||||
|
"RegionMarker" : {
|
||||||
|
"text-color" : "#928374",
|
||||||
|
"selected-text-color" : "#a89984",
|
||||||
|
"background-color" : "#1d2021"
|
||||||
|
},
|
||||||
|
"Information" : {
|
||||||
|
"text-color" : "#282828",
|
||||||
|
"selected-text-color" : "#282828",
|
||||||
|
"background-color" : "#83a598"
|
||||||
|
},
|
||||||
|
"Warning" : {
|
||||||
|
"text-color" : "#282828",
|
||||||
|
"selected-text-color" : "#282828",
|
||||||
|
"background-color" : "#fabd2f"
|
||||||
|
},
|
||||||
|
"Alert" : {
|
||||||
|
"text-color" : "#282828",
|
||||||
|
"selected-text-color" : "#282828",
|
||||||
|
"background-color" : "#cc241d",
|
||||||
|
"bold" : true
|
||||||
|
},
|
||||||
|
"Error" : {
|
||||||
|
"text-color" : "#cc241d",
|
||||||
|
"selected-text-color" : "#fb4934",
|
||||||
|
"underline" : true
|
||||||
|
},
|
||||||
|
"Others" : {
|
||||||
|
"text-color" : "#689d6a",
|
||||||
|
"selected-text-color" : "#8ec07c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background-color" : "#282828",
|
||||||
|
"editor-colors": {
|
||||||
|
"BackgroundColor" : "#282828",
|
||||||
|
"CodeFolding" : "#1d2021",
|
||||||
|
"BracketMatching" : "#a89984",
|
||||||
|
"CurrentLine" : "#32302f",
|
||||||
|
"IconBorder" : "#282828",
|
||||||
|
"IndentationLine" : "#504945",
|
||||||
|
"LineNumbers" : "#ebdbb2",
|
||||||
|
"CurrentLineNumber" : "#ebdbb2",
|
||||||
|
"MarkBookmark" : "#458588",
|
||||||
|
"MarkBreakpointActive" : "#cc241d",
|
||||||
|
"MarkBreakpointReached" : "#98971a",
|
||||||
|
"MarkBreakpointDisabled" : "#b16286",
|
||||||
|
"MarkExecution" : "#ebdbb2",
|
||||||
|
"MarkWarning" : "#d65d0e",
|
||||||
|
"MarkError" : "#cc241d",
|
||||||
|
"ModifiedLines" : "#fe8019",
|
||||||
|
"ReplaceHighlight" : "#b8bb26",
|
||||||
|
"SavedLines" : "#689d6a",
|
||||||
|
"SearchHighlight" : "#8ec07c",
|
||||||
|
"TextSelection" : "#504945",
|
||||||
|
"Separator" : "#504945",
|
||||||
|
"SpellChecking" : "#cc241d",
|
||||||
|
"TabMarker" : "#504945",
|
||||||
|
"TemplateBackground" : "#282828",
|
||||||
|
"TemplatePlaceholder" : "#98971a",
|
||||||
|
"TemplateFocusedPlaceholder" : "#b8bb26",
|
||||||
|
"TemplateReadOnlyPlaceholder" : "#fb4934",
|
||||||
|
"WordWrapMarker" : "#a89984"
|
||||||
|
}
|
||||||
|
}
|
37
index.qmd
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: "Nicole Dresselhaus"
|
||||||
|
listing:
|
||||||
|
sort: "date desc"
|
||||||
|
type: grid
|
||||||
|
categories: cloud
|
||||||
|
feed: true
|
||||||
|
date-format: long
|
||||||
|
max-description-length: 250
|
||||||
|
contents:
|
||||||
|
- "Writing"
|
||||||
|
- "Coding"
|
||||||
|
- "Health"
|
||||||
|
- "Uni"
|
||||||
|
- "Opinion"
|
||||||
|
- "Stuff"
|
||||||
|
image-placeholder: "./thumbs/placeholder.png"
|
||||||
|
image-height: "200"
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
other-links:
|
||||||
|
- text: Mastodon
|
||||||
|
icon: mastodon
|
||||||
|
href: "https://toot.kif.rocks/@Drezil"
|
||||||
|
rel: me
|
||||||
|
- text: Github
|
||||||
|
icon: github
|
||||||
|
href: "https://github.com/Drezil"
|
||||||
|
rel: me
|
||||||
|
---
|
||||||
|
|
||||||
|
Unsortierte Einsichten und Erfahrungen. Archiviert zum verlinken, späteren
|
||||||
|
Überdenken oder Diskutieren.
|
||||||
|
|
||||||
|
Keine Garantie auf Richtigkeit oder Trollfreiheit :>
|
||||||
|
|
||||||
|
## Letzte Posts
|
141
styles.scss
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*-- scss:defaults --*/
|
||||||
|
/* GRID VARIABLES */
|
||||||
|
// The left hand sidebar
|
||||||
|
$grid-sidebar-width: 250px !default;
|
||||||
|
// The main body
|
||||||
|
$grid-body-width: 800px !default;
|
||||||
|
// The right hand margin bar
|
||||||
|
$grid-margin-width: 250px !default;
|
||||||
|
// The gutter that appears between the above columns
|
||||||
|
$grid-page-gutter: 1.5em;
|
||||||
|
$grid-column-gutter-width: 3fr !default;
|
||||||
|
|
||||||
|
$grid-body-column-max: $grid-body-width !default;
|
||||||
|
$grid-body-column-min: quarto-math.min(500px, $grid-body-column-max) !default;
|
||||||
|
|
||||||
|
$grid-docked-body-width: $grid-body-column-max + 200px !default;
|
||||||
|
|
||||||
|
$grid-docked-wide-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-wide-body-column-max: $grid-docked-body-width !default;
|
||||||
|
$grid-docked-wide-body: minmax(
|
||||||
|
$grid-docked-wide-body-column-min,
|
||||||
|
$grid-docked-wide-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-wide-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-wide-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-wide-slim-body-column-min: $grid-body-column-min - 50px !default;
|
||||||
|
$grid-docked-wide-slim-body-column-max: $grid-body-column-max - 50px !default;
|
||||||
|
$grid-docked-wide-slim-body: minmax(
|
||||||
|
$grid-docked-wide-slim-body-column-min,
|
||||||
|
$grid-docked-wide-slim-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-wide-slim-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-wide-slim-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-wide-full-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-wide-full-body-column-max: $grid-docked-body-width !default;
|
||||||
|
$grid-docked-wide-full-body: minmax(
|
||||||
|
$grid-docked-wide-full-body-column-min,
|
||||||
|
$grid-docked-wide-full-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-wide-full-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-wide-full-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-wide-listing-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-wide-listing-body-column-max: $grid-docked-body-width !default;
|
||||||
|
$grid-docked-wide-listing-body: minmax(
|
||||||
|
$grid-docked-wide-listing-body-column-min,
|
||||||
|
$grid-docked-wide-listing-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-wide-listing-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-wide-listing-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-mid-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-mid-body-column-max: $grid-body-column-max - 50px !default;
|
||||||
|
$grid-docked-mid-body: minmax(
|
||||||
|
$grid-docked-mid-body-column-min,
|
||||||
|
$grid-docked-mid-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-mid-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-mid-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-mid-slim-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-mid-slim-body-column-max: $grid-docked-body-width !default;
|
||||||
|
$grid-docked-mid-slim-body: minmax(
|
||||||
|
$grid-docked-mid-slim-body-column-min,
|
||||||
|
$grid-docked-mid-slim-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-mid-slim-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-mid-slim-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-mid-full-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-mid-full-body-column-max: $grid-docked-body-width !default;
|
||||||
|
$grid-docked-mid-full-body: minmax(
|
||||||
|
$grid-docked-mid-full-body-column-min,
|
||||||
|
$grid-docked-mid-full-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-mid-full-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-mid-full-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$grid-docked-mid-listing-body-column-min: $grid-body-column-min !default;
|
||||||
|
$grid-docked-mid-listing-body-column-max: $grid-body-column-max - 50px !default;
|
||||||
|
$grid-docked-mid-listing-body: minmax(
|
||||||
|
$grid-docked-mid-listing-body-column-min,
|
||||||
|
$grid-docked-mid-listing-body-column-max
|
||||||
|
) !default;
|
||||||
|
$grid-docked-mid-listing-body-gutter-start: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
$grid-docked-mid-listing-body-gutter-end: minmax(
|
||||||
|
$grid-page-gutter,
|
||||||
|
$grid-column-gutter-width
|
||||||
|
);
|
||||||
|
|
||||||
|
$primary: scale-color(#fc0fc0, $chroma: -50%, $lightness: -20%, $space: oklch);
|
||||||
|
$link-color: scale-color(#fc0, $chroma: -50%, $lightness: -10%, $space: oklch);
|
||||||
|
|
||||||
|
/*-- scss:rules --*/
|
BIN
thumbs/placeholder.png
Normal file
After Width: | Height: | Size: 380 KiB |
BIN
thumbs/writing_documentation.png
Normal file
After Width: | Height: | Size: 269 KiB |
BIN
thumbs/writing_ner4all-case-study.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
thumbs/writing_obsidian-rag.png
Normal file
After Width: | Height: | Size: 322 KiB |