From fec03e94e75921bb195a63b048d18eb4c7e6c45d Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Thu, 27 Jul 2023 16:04:42 +0200
Subject: [PATCH] feat: #1

---
 .../run_example_prepare_.xml                  | 16 ++++
 README.md                                     | 27 +++++-
 application/source/command.go                 | 15 ++-
 application/source/constants/attributes.go    |  3 +
 application/source/html/generate.go           |  6 +-
 application/source/template/prepare.go        | 83 ++++++++++++----
 application/source/util/html.go               |  7 +-
 .../{template/data.yaml => pages/en.yaml}     | 96 ++++++++++++++++---
 .../examples/example1/template/test.html      | 36 +++++--
 devenv.lock                                   | 18 ++--
 10 files changed, 242 insertions(+), 65 deletions(-)
 create mode 100644 .idea/runConfigurations/run_example_prepare_.xml
 create mode 100644 application/source/constants/attributes.go
 rename development/examples/example1/{template/data.yaml => pages/en.yaml} (69%)

diff --git a/.idea/runConfigurations/run_example_prepare_.xml b/.idea/runConfigurations/run_example_prepare_.xml
new file mode 100644
index 0000000..f41e2e8
--- /dev/null
+++ b/.idea/runConfigurations/run_example_prepare_.xml
@@ -0,0 +1,16 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="run example prepare " type="GoApplicationRunConfiguration" factoryName="Go Application">
+    <module name="bob" />
+    <working_directory value="$PROJECT_DIR$/../../alvine/local-dev/components/alvine/apps/test/source" />
+    <parameters value="--verbose template prepare --input $PROJECT_DIR$/development/examples/example1/template/ --output $PROJECT_DIR$/development/examples/example1/build --data-file=$PROJECT_DIR$/development/examples/example1/pages/en.yaml" />
+    <EXTENSION ID="com.fapiko.jetbrains.plugins.better_direnv.runconfigs.GolandRunConfigurationExtension">
+      <option name="DIRENV_ENABLED" value="false" />
+      <option name="DIRENV_TRUSTED" value="false" />
+    </EXTENSION>
+    <kind value="PACKAGE" />
+    <package value="gitlab.schukai.com/oss/bob" />
+    <directory value="$PROJECT_DIR$" />
+    <filePath value="$PROJECT_DIR$/application/source/main.go" />
+    <method v="2" />
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/README.md b/README.md
index 02d1f5f..bbc39ee 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Bob
 
-bob is an HTML and HTML fragment builder
+Bob is an HTML and HTML fragment builder
 
 ## Documentation
 
@@ -14,7 +14,8 @@ wget -O ~/.local/bin/bob http://download.schukai.com/tools/bob/bob-$( uname -s |
 
 ### Nix/Flake Support
 
-This repository contains a file called flake.nix. You can install this program using the nix package manager.
+This repository contains a file called flake.nix. You can install this program 
+using the [nix package manager](https://nixos.org/).
 
 ## Usage
 
@@ -27,7 +28,7 @@ bob template prepare --input ./templates/ --output ./output/ --data-file ./data.
 ```
 
 This will create files in the `./output/` directory with all parsed templates from `./templates/` directory.
-Also, a data YAML. This data YAML is used to generate the final files.
+Also, a data YAML. This data YAML is used to generate the final files with the `bob html generate` command.
 
 This command prepares the title, description, keywords, and other metadata for the templates.
 Furthermore, it will parse the templates for images, anchors, and text.
@@ -43,6 +44,21 @@ If the argument `--data-file` is not set, the data YAML will be written to `./ou
 | `<a href="https://gitlab.schukai.com/oss/bob">`                               | `<a href="https://gitlab.schukai.com/oss/bob" data-attributes="href path:a.id1004.href">`                                                       |
 | `<p>Bob is a html and html fragment builder</p>`                              | `<p><span data-attributes="text path:p.id1005.text">Bob is a html and html fragment builder</span></p>`                                         |
 
+If you want to translate the text, you can copy the default `data.yaml` to a new file and translate the text there.
+
+A good practice is to use the language code as the file name. 
+For example, `de.yaml` for German, `en.yaml` for English, etc.
+
+Beside text, images and metadata, special attributes are also extracted.
+For example, the Monster datatable headers `data-monster-head` are extracted.
+
+```html
+<monster-datatable>
+    <template id="datatable-order-list-row">
+        <div data-monster-head="OID" ...></a></div>
+    ...
+``` 
+
 
 #### HTML
 
@@ -54,9 +70,10 @@ will be processed.
 
 
 ```bash
-bob html generate --input ./input/ --output ./output/
+bob html generate --input ./input/ --output ./output/ --data-files ./pages/
 ```
 
+If the `--data-files' attribute is not defined, the `--input' directory is used.
 
 The yaml looks like:
 
@@ -117,8 +134,8 @@ test1.html:
 
 The translations are set in a json inside a script tag.
 
-The modifications run last. Here you can remove tags, add inhaklt and set attributes.
 
+The `modifications' rules are executed last. Here you can remove tags, add content and set attributes.
 
 
 ##### Sync
diff --git a/application/source/command.go b/application/source/command.go
index fdec0b7..e4061c2 100644
--- a/application/source/command.go
+++ b/application/source/command.go
@@ -20,13 +20,14 @@ type Definition struct {
 		Prepare struct {
 			Input    string `short:"i" long:"input" description:"Directory with html files to prepare" required:"true"`
 			Output   string `short:"o" long:"output" description:"Directory to save prepared html files" required:"true"`
-			DataFile string `short:"d" long:"data-file" description:"Name of the data file to use, default is data.yaml"`
+			DataFile string `short:"d" long:"data-file" description:"Name of the main data file" default:"data.yaml"`
 		} `command:"prepare" description:"Prepare content from a file" call:"PrepareTemplate"`
 	} `command:"template" description:"Template commands"`
 	HTML struct {
 		Generate struct {
-			Input  string `short:"i" long:"input" description:"Directory with prepared html files" required:"true"`
-			Output string `short:"o" long:"output" description:"Directory to save generated template files" required:"true"`
+			Input     string `short:"i" long:"input" description:"Directory with prepared html files" required:"true"`
+			Output    string `short:"o" long:"output" description:"Directory to save generated template files" required:"true"`
+			DataFiles string `short:"d" long:"data-files" description:"Directory with data files" required:"true"`
 		} `command:"generate" description:"Generate html files from a file" call:"GenerateHTML"`
 		Sync struct {
 			Specification string `short:"s" long:"specification" description:"Specification file" required:"true"`
@@ -55,7 +56,11 @@ func (d *Definition) SyncHTML(s *xflags.Settings[Definition]) {
 }
 func (d *Definition) GenerateHTML(s *xflags.Settings[Definition]) {
 
-	err := filepath.Walk(d.HTML.Generate.Input, func(p string, info os.FileInfo, err error) error {
+	if d.HTML.Generate.DataFiles == "" {
+		d.HTML.Generate.DataFiles = d.HTML.Generate.Input
+	}
+
+	err := filepath.Walk(d.HTML.Generate.DataFiles, func(p string, info os.FileInfo, err error) error {
 
 		if err != nil {
 			return err
@@ -70,7 +75,7 @@ func (d *Definition) GenerateHTML(s *xflags.Settings[Definition]) {
 			return nil
 		}
 
-		return html2.GenerateFiles(p, d.HTML.Generate.Output)
+		return html2.GenerateFiles(p, d.HTML.Generate.Input, d.HTML.Generate.Output)
 	})
 
 	if err != nil {
diff --git a/application/source/constants/attributes.go b/application/source/constants/attributes.go
new file mode 100644
index 0000000..c383e7b
--- /dev/null
+++ b/application/source/constants/attributes.go
@@ -0,0 +1,3 @@
+package constants
+
+const DataBobReferenceAttributeKey = "data-bob-reference"
diff --git a/application/source/html/generate.go b/application/source/html/generate.go
index 185c10b..40c5796 100644
--- a/application/source/html/generate.go
+++ b/application/source/html/generate.go
@@ -12,9 +12,9 @@ import (
 	"strings"
 )
 
-func GenerateFiles(dataPath, out string) error {
+func GenerateFiles(dataPath, templates, out string) error {
 
-	dir := path.Dir(dataPath)
+	templatesDir := path.Dir(templates)
 
 	yamlFile, err := os.ReadFile(dataPath)
 	if err != nil {
@@ -27,7 +27,7 @@ func GenerateFiles(dataPath, out string) error {
 	}
 
 	for name, page := range storage {
-		p := path.Join(dir, name)
+		p := path.Join(templatesDir, name)
 		generatedHtml, err := Generate(page, p)
 		if err != nil {
 			return err
diff --git a/application/source/template/prepare.go b/application/source/template/prepare.go
index fb7fd54..b13d0fb 100644
--- a/application/source/template/prepare.go
+++ b/application/source/template/prepare.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/andybalholm/cascadia"
+	"gitlab.schukai.com/oss/bob/constants"
 	"gitlab.schukai.com/oss/bob/types"
 	"gitlab.schukai.com/oss/bob/util"
 	"golang.org/x/net/html"
@@ -173,11 +174,11 @@ func prepareAnchors(node *html.Node, storage *types.PageData) {
 		hreflang := util.GetAttribute(n.Attr, "hreflang")
 		href := util.GetAttribute(n.Attr, "href")
 
-		id := util.GetOrCreateId(n, title+hreflang+href)
+		id := util.GetOrCreateReference(n, title+hreflang+href)
 		if id == "" {
 			id, _ = util.RandomString(8)
-			n.Attr = removeAttribute(n.Attr, "id")
-			n.Attr = append(n.Attr, html.Attribute{Key: "id", Val: id})
+			n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
+			n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
 		}
 
 		setDataAttributesAttribute(n, attributeAttributes, "href", "path:anchors."+id+".href")
@@ -231,11 +232,11 @@ func prepareImages(node *html.Node, storage *types.PageData) {
 		title := util.GetAttribute(n.Attr, "title")
 		source := util.GetAttribute(n.Attr, "src")
 
-		id := util.GetOrCreateId(n, alt+title+source)
+		id := util.GetOrCreateReference(n, alt+title+source)
 		if id == "" {
 			id, _ = util.RandomString(8)
-			n.Attr = removeAttribute(n.Attr, "id")
-			n.Attr = append(n.Attr, html.Attribute{Key: "id", Val: id})
+			n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
+			n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
 		}
 
 		setDataAttributesAttribute(n, attributeAttributes, "src", "path:content."+id+".src")
@@ -284,7 +285,7 @@ func prepareTranslationJson(node *html.Node, storage *types.PageData) {
 
 	for _, n := range list {
 
-		id := util.GetAttribute(n.Attr, "id")
+		id := util.GetAttribute(n.Attr, constants.DataBobReferenceAttributeKey)
 		typ := util.GetAttribute(n.Attr, "type")
 
 		n.Attr = removeAttribute(n.Attr, attributeReplace)
@@ -377,19 +378,7 @@ func runNodes(n *html.Node, storage *types.PageData) {
 
 }
 
-func checkNodes(n *html.Node, storage *types.PageData) {
-	if n.Parent != nil {
-		if n.Parent.Type == html.ElementNode {
-			if n.Parent.Data == "script" || n.Parent.Data == "style" {
-				return
-			}
-		}
-	}
-
-	if n.Type != html.TextNode {
-		return
-	}
-
+func handleTextNode(n *html.Node, storage *types.PageData) {
 	content := strings.TrimSpace(n.Data)
 	if content == "" {
 		return
@@ -406,7 +395,7 @@ func checkNodes(n *html.Node, storage *types.PageData) {
 		Data: atom.Span.String(),
 		Attr: []html.Attribute{
 			{
-				Key: "id",
+				Key: constants.DataBobReferenceAttributeKey,
 				Val: id,
 			},
 			{
@@ -424,6 +413,58 @@ func checkNodes(n *html.Node, storage *types.PageData) {
 		Id:   id,
 		Text: content,
 	})
+}
+
+func checkNodes(n *html.Node, storage *types.PageData) {
+	if n.Parent != nil {
+		if n.Parent.Type == html.ElementNode {
+			if n.Parent.Data == "script" || n.Parent.Data == "style" {
+				return
+			}
+		}
+	}
+
+	if n.Type == html.TextNode {
+		handleTextNode(n, storage)
+	} else if n.Type == html.ElementNode {
+		switch n.Data {
+		case "monster-datatable":
+			checkMonsterDatatableHead(n, storage)
+		}
+
+	}
+
+}
+
+func checkMonsterDatatableHead(n *html.Node, storage *types.PageData) {
+	selector, err := cascadia.Parse("[data-monster-head]")
+	if err != nil {
+		return
+	}
+
+	list := cascadia.QueryAll(n, selector)
+	if list == nil {
+		return
+	}
+
+	for _, div := range list {
+		id := util.GetAttribute(div.Attr, constants.DataBobReferenceAttributeKey)
+		if id == "" {
+			id = util.GetNextId()
+			div.Attr = append(div.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id})
+		}
+
+		head := util.GetAttribute(div.Attr, "data-monster-head")
+
+		div.Attr = removeAttribute(div.Attr, "data-attributes")
+		div.Attr = append(div.Attr, html.Attribute{Key: "data-attributes", Val: "data-monster-head path:text." + id + ".text"})
+
+		storage.Text = append(storage.Text, types.Text{
+			Id:   id,
+			Text: head,
+		})
+
+	}
 
 }
 
diff --git a/application/source/util/html.go b/application/source/util/html.go
index 26753f2..d206b96 100644
--- a/application/source/util/html.go
+++ b/application/source/util/html.go
@@ -2,6 +2,7 @@ package util
 
 import (
 	"errors"
+	"gitlab.schukai.com/oss/bob/constants"
 	"golang.org/x/net/html"
 	"os"
 	"path"
@@ -37,11 +38,11 @@ func GetNextId() string {
 
 }
 
-func GetOrCreateId(node *html.Node, text string) string {
+func GetOrCreateReference(node *html.Node, text string) string {
 
 	var err error
 
-	nodeId := GetAttribute(node.Attr, "id")
+	nodeId := GetAttribute(node.Attr, constants.DataBobReferenceAttributeKey)
 	if nodeId != "" {
 		return nodeId
 	}
@@ -51,7 +52,7 @@ func GetOrCreateId(node *html.Node, text string) string {
 		nodeId = GetNextId()
 	}
 
-	node.Attr = append(node.Attr, html.Attribute{Key: "id", Val: nodeId})
+	node.Attr = append(node.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: nodeId})
 	return nodeId
 
 }
diff --git a/development/examples/example1/template/data.yaml b/development/examples/example1/pages/en.yaml
similarity index 69%
rename from development/examples/example1/template/data.yaml
rename to development/examples/example1/pages/en.yaml
index 5b0608a..d7c632d 100755
--- a/development/examples/example1/template/data.yaml
+++ b/development/examples/example1/pages/en.yaml
@@ -1,3 +1,83 @@
+test.html:
+    export: en/test.html
+    lang: en
+    title: Bad Request
+    meta:
+        author: schukai GmbH
+        description: The request was malformed or invalid.
+    images:
+        - id: tickyesdata-image-gi-4013311193
+          source: |-
+            data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
+              V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
+              PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7
+          alt: tick
+          title: "yes"
+    anchors:
+        - id: yes-a-html
+          href: /a.html
+          hreflang: ""
+          title: "Yes"
+        - id: QPCI6WO0
+          href: ""
+          hreflang: ""
+          title: ""
+    text:
+        - text: Bad Request
+          id: bad-request
+        - text: The request was incorrect, the server could not process it.
+          id: the-request-was-inco-2640993422
+        - text: Try submitting your
+          id: try-submitting-your
+        - text: request
+          id: request
+        - text: again (sometimes this helps).
+          id: again-sometimes-this-2086042570
+        - text: Contact support if you continue to have problems.
+          id: contact-support-if-y-3404332025
+        - text: |-
+            If you received this message as a result of a request to the server API, then check the structure
+                            against the documentation.
+          id: if-you-received-this-423958995
+        - text: 'You   can   try    the following steps:'
+          id: you-can-try-the-foll-3363859033
+        - text: OID
+          id: id1000
+        - text: date
+          id: id1001
+        - text: username
+          id: id1002
+        - text: customer
+          id: id1003
+        - text: zipcode
+          id: id1004
+        - text: city
+          id: id1005
+        - text: country
+          id: id1006
+        - text: street
+          id: id1007
+        - text: order state
+          id: id1008
+        - text: workflow state
+          id: id1009
+        - text: total
+          id: id1010
+        - text: company
+          id: id1011
+        - text: channel order number
+          id: id1012
+        - text: RESUBMISSIONDATE!!
+          id: id1013
+        - text: payment type
+          id: id1014
+        - text: ','
+          id: id1015
+    translations: []
+    modifications:
+        remove: []
+        add: []
+        attributes: []
 test1.html:
     export: de/test1.html
     lang: de
@@ -44,7 +124,7 @@ test1.html:
         - text: 'You   can   try    the following steps:'
           id: you-can-try-the-foll-3363859033
     translations:
-        - id: translations
+        - id: ""
           type: application/json
           translations:
             key1: value1
@@ -53,15 +133,9 @@ test1.html:
                 one: value3
                 two: value4
     modifications:
-        remove: 
-          - .gradient
-        add: 
-          - selector: .infobox
-            html: <b><span>GURKE</span></b>
-        attributes: 
-          - selector: '#mainscript'
-            name: type
-            value: YES
+        remove: []
+        add: []
+        attributes: []
 test2.html:
     export: en/test2.html
     lang: en
@@ -167,7 +241,7 @@ test4.html:
         - text: one-time password
           id: one-time-password
         - text: .
-          id: id1000
+          id: id1016
     translations: []
     modifications:
         remove: []
diff --git a/development/examples/example1/template/test.html b/development/examples/example1/template/test.html
index ab67804..3508e76 100644
--- a/development/examples/example1/template/test.html
+++ b/development/examples/example1/template/test.html
@@ -40,26 +40,26 @@
     <img width="16" height="16" alt="tick" title="yes" src="data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm
   V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ
   PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7" id="tickyesdata-image-gi-4013311193" data-attributes="alt path:content.tickyesdata-image-gi-4013311193.alt,src path:content.tickyesdata-image-gi-4013311193.src,title path:content.tickyesdata-image-gi-4013311193.title"/>
-    <h1 class="deco"><span id="bad-request" data-replace-self="path:text.bad-request.text">Bad Request</span></h1>
-    <p><span id="the-request-was-inco-2640993422" data-replace-self="path:text.the-request-was-inco-2640993422.text">The request was incorrect, the server could not process it.
-    </span></p><p>
+    <h1 class="deco">Bad Request</h1>
+    <p>The request was incorrect, the server could not process it.
+    </p><p>
 </p><div class="infobox">
     <div>
 
         <ul>
 
-            <li><span id="try-submitting-your" data-replace-self="path:text.try-submitting-your.text">Try submitting your </span><a href="/a.html" title="Yes" id="yes-a-html" 
+            <li>Try submitting your<a href="/a.html" title="Yes" id="yes-a-html" 
 data-attributes="href path:anchors.yes-a-html.href,title path:anchors.yes-a-html.title,hreflang path:anchors.yes-a-html.hreflang">request</a> again (sometimes this helps).</li>
-            <li><span id="contact-support-if-y-3404332025" data-replace-self="path:text.contact-support-if-y-3404332025.text">Contact support if you continue to have problems.</span></li>
+            <li>Contact support if you continue to have problems.</li>
 
-            <li><span id="if-you-received-this-423958995" data-replace-self="path:text.if-you-received-this-423958995.text">If you received this message as a result of a request to the server API, then check the structure
+            <li>If you received this message as a result of a request to the server API, then check the structure
                 against the documentation.
-            </span></li>
+            </li>
 
 
         </ul>
 
-        <p><span id="you-can-try-the-foll-3363859033" data-replace-self="path:text.you-can-try-the-foll-3363859033.text">You   can   try    the following steps:</span></p>
+        <p>You   can   try    the following steps:</p>
         
     </div>
 </div>
@@ -68,5 +68,25 @@ data-attributes="href path:anchors.yes-a-html.href,title path:anchors.yes-a-html
 </monster-state>
 
 
+<monster-datatable id="datatable-order-list" data-monster-datasource-selector="#orderListDatasource">
+    <template id="datatable-order-list-row">
+        <div data-monster-head="OID" data-monster-mode="fixed" data-monster-sortable="oid" data-monster-grid-template="0.5fr"><a data-monster-attributes="href path:datatable-order-list-row.oid | tostring | prefix:/app/commerce/order-detail?oid=" data-monster-replace="path:datatable-order-list-row.oid"></a></div>
+        <div data-monster-head="date" data-monster-sortable="orderDate" data-monster-replace="path:datatable-order-list-row.orderDate | datetimeformat"></div>
+        <div data-monster-head="username" data-monster-mode="hidden" data-monster-replace="path:datatable-order-list-row.userName"></div>
+        <div data-monster-head="customer">, </div>
+        <div data-monster-head="zipcode" data-monster-replace="path:datatable-order-list-row.deliveryAddressZipcode"></div>
+        <div data-monster-head="city" data-monster-replace="path:datatable-order-list-row.deliveryAddressCity"></div>
+        <div data-monster-head="country" data-monster-replace="path:datatable-order-list-row.deliveryAddressCountry | prefix:country_ | i18n"></div>
+        <div data-monster-head="street" data-monster-mode="hidden" data-monster-replace="path:datatable-order-list-row.deliveryAddressAddress1"></div>
+        <div data-monster-head="order state" data-monster-align="end"></div>
+        <div data-monster-head="workflow state" data-monster-align="end" data-monster-replace="path:datatable-order-list-row.workflowState"></div>
+        <div data-monster-head="total" data-monster-align="end"></div>
+        <div data-monster-head="company" data-monster-grid-template="0.8fr" data-monster-replace="path:datatable-order-list-row.companySHID | tostring | prefix:&lt;img src=&#39;/alvine/upload/company/files/ | suffix:/shopicon.gif&#39;&gt;"></div>
+        <div data-monster-head="channel order number" data-monster-replace="path:datatable-order-list-row.channelOrderID"></div>
+        <div data-monster-head="resubmissionDate" data-monster-mode="hidden"></div>
+        <div data-monster-head="payment type" data-monster-replace="path:datatable-order-list-row.localStrings.paymentType"></div>
+    </template>
+</monster-datatable>
+
 
 </body></html>
\ No newline at end of file
diff --git a/devenv.lock b/devenv.lock
index fd80158..8e0ea4a 100644
--- a/devenv.lock
+++ b/devenv.lock
@@ -3,11 +3,11 @@
     "devenv": {
       "locked": {
         "dir": "src/modules",
-        "lastModified": 1689667485,
-        "narHash": "sha256-tLNoMRSPLlW1D4wgNpSIPUUHd6x1GdjAhETLpRTKnfo=",
+        "lastModified": 1690413082,
+        "narHash": "sha256-CPR3WcnrrIiDZJiMo4RlyZB0M3576pHmtlTUnMUTugA=",
         "owner": "cachix",
         "repo": "devenv",
-        "rev": "6f8add968bc12bf81d845eb7dc684a0733bb1518",
+        "rev": "148c4a21e50428728e97f3cdf59166b6007db8a7",
         "type": "github"
       },
       "original": {
@@ -74,11 +74,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1689631193,
-        "narHash": "sha256-AGSkBZaiTODQc8eT1rZDrQIjtb8JtFwJ0wVPzArlrnM=",
+        "lastModified": 1690441914,
+        "narHash": "sha256-Ac+kJQ5z9MDAMyzSc0i0zJDx2i3qi9NjlW5Lz285G/I=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "57695599bdc4f7bfe5d28cfa23f14b3d8bdf8a5f",
+        "rev": "db8672b8d0a2593c2405aed0c1dfa64b2a2f428f",
         "type": "github"
       },
       "original": {
@@ -115,11 +115,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1689668210,
-        "narHash": "sha256-XAATwDkaUxH958yXLs1lcEOmU6pSEIkatY3qjqk8X0E=",
+        "lastModified": 1690464206,
+        "narHash": "sha256-38V4kmOh6ikpfGiAS+Kt2H/TA2DubSqE66veP/jmB4Q=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "eb433bff05b285258be76513add6f6c57b441775",
+        "rev": "9289996dcac62fd45836db7c07b87d2521eb526d",
         "type": "github"
       },
       "original": {
-- 
GitLab