diff --git a/README.md b/README.md index bbc39eed756e6af2e53904dc5815f5ea21150517..b43faffb69978d1327961b245e848af29a2f1fa5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ To check out docs and examples, visit [gitlab.schukai.com/oss/bob](https://gitla ## Installation ```bash -wget -O ~/.local/bin/bob http://download.schukai.com/tools/bob/bob-$( uname -s | tr [:upper:] [:lower:])-$(echo `uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/`) && chmod u+x ~/.local/bin/bob +wget -O ~/.local/bin/bob \ + http://download.schukai.com/tools/bob/bob-$( uname -s | tr [:upper:] [:lower:])-$(echo `uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/`) && chmod u+x ~/.local/bin/bob ``` ### Nix/Flake Support @@ -24,7 +25,10 @@ using the [nix package manager](https://nixos.org/). #### Prepare ```bash -bob template prepare --input ./templates/ --output ./output/ --data-file ./data.yaml +bob template prepare \ + --input ./templates/ \ + --output ./output/ \ + --data-file ./data.yaml \ ``` This will create files in the `./output/` directory with all parsed templates from `./templates/` directory. @@ -59,6 +63,25 @@ For example, the Monster datatable headers `data-monster-head` are extracted. ... ``` +If you want to add Javascript translations, a consecutive ID is assigned if not +specified separately. This is not ideal for several reasons. It is better to assign +your own ID. To be independent of HTML, JavaScript and CSS selectors, there is a +separate attribute `data-bob-reference` for this. + +```html +<script type="application/json" + data-monster-role="translations" + data-bob-reference="my-translation"> +{ + "key1": "value1", + "key2": "value2", + "key3": { + "one": "value3", + "two": "value4" + } +} +``` + #### HTML @@ -161,7 +184,8 @@ sync: - title ``` -With the above specification, the `head` node from `./source.html` will be synced to all files in `./` except `./source.html`. +With the above specification, the `head` node from `./source.html` will +be synced to all files in `./` except `./source.html`. Furthermore, the `title` node will be kept. Relative paths are relative to the specification file. Absolute paths are absolute, obviously. diff --git a/devenv.nix b/devenv.nix index 7892c37d46234c732f02172885d179b1d37f500e..b5bfeca8dc573ce238fb04d2cd1e9946a8625182 100644 --- a/devenv.nix +++ b/devenv.nix @@ -33,6 +33,7 @@ httpie netcat memcached + treefmt fd ]; diff --git a/examples/example1/pages/en.yaml b/examples/example1/pages/en.yaml deleted file mode 100755 index f49c00bad86635af2eba4486b860b0d12c336fc6..0000000000000000000000000000000000000000 --- a/examples/example1/pages/en.yaml +++ /dev/null @@ -1,249 +0,0 @@ -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 - title: TESTx - 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: test-link-test-html - href: /test.html - hreflang: "" - title: test-link - - id: yes-a-html - href: /a.html - hreflang: "" - title: "Yes" - text: - - text: test-link - id: test-link - - text: Bad xxxx - id: bad-xxxx - - 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 - translations: - - id: "" - type: application/json - translations: - key1: value1 - key2: value2 - key3: - one: value3 - two: value4 - modifications: - remove: [] - add: [] - attributes: [] -test2.html: - export: en/test2.html - lang: en - title: TEST2 - meta: - author: sch2ukai GmbH - description: The 2request 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" - text: - - text: Bad xxxx - id: bad-xxxx - - 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 - translations: [] - modifications: - remove: [] - add: [] - attributes: [] -test3.html: - export: en/test3.html - lang: en - title: TEST3 - meta: - author: schukai3 GmbH - description: The 3request 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" - text: - - text: Bad xxxx - id: bad-xxxx - - 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 - translations: [] - modifications: - remove: [] - add: [] - attributes: [] -test4.html: - export: en/test4.html - lang: en - title: "" - meta: {} - images: [] - anchors: - - id: a-html - href: a.html - hreflang: "" - title: "" - text: - - text: Das ist ein Text - id: das-ist-ein-text - - text: one-time password - id: one-time-password - - text: . - id: id1016 - translations: [] - modifications: - remove: [] - add: [] - attributes: [] diff --git a/examples/example2/config/snippet.yaml b/examples/example2/config/snippet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bb21f81000bdbae6dfdb8f1260594e3536451d84 --- /dev/null +++ b/examples/example2/config/snippet.yaml @@ -0,0 +1,13 @@ +snippet: + - + source: ../template/test.html + selector: 'monster-state' + destination: ../snippets/meta/container.html + attribute: + - selector: 'li' + name: 'data-state' + value: 'monster' + replacement: + - + selector: 'li>span' + content: '!!!!!!!!!!!!!!!!!!' diff --git a/examples/example2/config/spec1.yaml b/examples/example2/config/spec1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..588884439418acb1b7eb1b7c852d7e331cd125df --- /dev/null +++ b/examples/example2/config/spec1.yaml @@ -0,0 +1,24 @@ +sync: + + - source: + path: '../original/test1.html' + selector: head + destination: + path: '../original/' + exclude: + - ../original/test1.html + keep: + - title + - meta[name=description] + - meta[name=author] + +# - source: +# path: '../original/test1.html' +# selector: '.deco' +# destination: +# path: '../original/' +# exclude: +# - ../original/test1.html + + + diff --git a/examples/example2/pages/en.yaml b/examples/example2/pages/en.yaml new file mode 100755 index 0000000000000000000000000000000000000000..7e477d0e3bb0536ff28b70fdd2f65f592e531e1c --- /dev/null +++ b/examples/example2/pages/en.yaml @@ -0,0 +1,93 @@ +test.html: + export: en/test.html + lang: en + title: 333333 + meta: + author: 1111 + description: 22222 + images: + - id: tickyesdata-image-gi-3753669760 + source: |- + data:image/gif;base64,R0lGODdhEAAQAMwAAPj7+FmhUYjNfGuxYYDJdYTIeanOpT+DOTuANXi/bGOrWj6CONzv2sPjv2Cm + V1unU4zPgI/Sg6DJnJ3ImTh8Mtbs00aNP1CZSGy0YqLEn47RgXW8amasW7XWsmmvX2iuXiwAAAAAEAAQAAAFVyAgjmRpnihqGCkpDQ + PbGkNUOFk6DZqgHCNGg2T4QAQBoIiRSAwBE4VA4FACKgkB5NGReASFZEmxsQ0whPDi9BiACYQAInXhwOUtgCUQoORFCGt/g4QAIQA7 + alt: tick + title: "yes" + anchors: + - id: mmmm + href: mmmm + hreflang: "" + title: "" + - id: yes-a-html + href: /a.html + hreflang: "" + title: "Yes" + - id: struct-a-data-monste-1746242989 + 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: GURKE + id: gurke + - 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: oid + - text: date + id: date + - text: username + id: username + - text: customer + id: customer + - text: zipcode + id: zipcode + - text: city + id: city + - text: country + id: country + - text: street + id: street + - text: order state + id: order-state + - text: workflow state + id: workflow-state + - text: total + id: total + - text: company + id: company + - text: channel order number + id: channel-order-number + - text: resubmissionDate + id: resubmission-date + - text: payment type + id: payment-type + translations: + - id: the-translation + type: application/json + translations: + key1: translation1 + key2: + other: translation2 + key3: + other: translation3 + key5: translation4 + modifications: + remove: [] + add: [] + attributes: [] diff --git a/examples/example2/snippets/meta/container.html b/examples/example2/snippets/meta/container.html new file mode 100644 index 0000000000000000000000000000000000000000..2424762369803336a154c2d11af0f787e9c8df13 --- /dev/null +++ b/examples/example2/snippets/meta/container.html @@ -0,0 +1,27 @@ +<monster-state> + <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> +</p><div class="infobox"> + <div> + + <ul> + + <li data-state="monster">!!!!!!!!!!!!!!!!!!<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 data-state="monster">!!!!!!!!!!!!!!!!!!</li> + + <li data-state="monster">!!!!!!!!!!!!!!!!!!</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> + + </div> +</div> + + +</monster-state> \ No newline at end of file diff --git a/examples/example2/template/test.html b/examples/example2/template/test.html new file mode 100644 index 0000000000000000000000000000000000000000..8a4265b264e95287154510ffb592e02fb2397b4b --- /dev/null +++ b/examples/example2/template/test.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<html lang="en" data-attributes="lang path:lang"> +<head> + + <style> + *:not(:defined) { + visibility: hidden; + } + </style> + + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + + <meta name="robots" content="noindex"/> + + <link rel="icon" type="image/x-icon" href="/asset/icon/favicon.svg"/> + <meta name="theme-color" content="#c10000"/> + <link rel="apple-touch-icon" sizes="180x180" href="/asset/icon/apple-touch-icon.png"/> + <link rel="icon" type="image/png" sizes="32x32" href="/asset/icon/favicon-32x32.png"/> + <link rel="icon" type="image/png" sizes="16x16" href="/asset/icon/favicon-16x16.png"/> + <link rel="mask-icon" href="/asset/icon/safari-pinned-tab.svg" color="#fd1d1d"/> + <meta name="msapplication-TileColor" content="#fd1d1d"/> + <meta name="msapplication-TileImage" content="/asset/icon/mstile-144x144.png"/> + + <title data-replace="path:title">Bad Request</title> + <meta name="description" content="The request was malformed or invalid." + data-attributes="content path:meta.description"/> + <meta name="author" content="schukai GmbH" data-attributes="content path:meta.author"/> + + <link rel="icon" href="/favicon.ico"/> + <link rel="icon" href="/favicon.svg" type="image/svg+xml"/> + <link rel="apple-touch-icon" href="/apple-touch-icon.png"/> + + <script src="/script/main.mjs" type="module"></script> + + <script type="application/json" data-monster-role="translations" data-bob-reference="the-translation"> + { + "key1": "translation1", + "key2": { + "other": "translation2" + }, + "key3": { + "other": "translation3" + }, + "key5": "translation4" + } + </script> + + +</head> + +<body> +<header> + <div class="gradient"></div> +</header> + +<monster-state> + <a href="mmmm"><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"/></a> + <h1 class="deco">Bad Request</h1> + <p>The request was incorrect, the server could not process it. + </p> + <p> + </p> + + <a href="mmmm">GURKE</a> + + <div class="infobox"> + <div> + + <ul> + + <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>Contact support if you continue to have problems.</li> + + <li>If you received this message as a result of a request to the server API, then check the structure + against the documentation. + </li> + + + </ul> + + <p>You can try the following steps:</p> + <p>You can try the following steps:</p> + + </div> + </div> + + +</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">auto</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="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:<img src='/alvine/upload/company/files/ | suffix:/shopicon.gif'>"></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/flake.nix b/flake.nix index c2e4d488fc1be967dfe1d2fd6ab039213be8457a..22aa4fa81faec05ada91781d868fb9e34bb91579 100644 --- a/flake.nix +++ b/flake.nix @@ -28,23 +28,66 @@ packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; - hashes = import ./hashes.nix; + + + projectDefinition = import ./project.nix; + + projectHash = + if (self ? shortRev) + then self.shortRev + else "dirty"; + + lastModifiedDate = + self.lastModifiedDate or self.lastModified or "19700101"; + sourcePath = ././source; + in { bob = pkgs.buildGoModule { - name = "bob"; + pname = projectDefinition.name; + version = projectDefinition.version; # In 'nix develop', we don't need a copy of the source tree # in the Nix store. - src = ././source; - vendorSha256 = hashes.hashValue; + src = sourcePath; + + + tags = []; # add your tags here (eq "netgo" "osusergo" "static_build") + + ldflags = [ + "-X '${projectDefinition.modulePath}/internal/release.version=${projectDefinition.version}'" + "-X '${projectDefinition.modulePath}/internal/release.commit=${projectHash}'" + "-X '${projectDefinition.modulePath}/internal/release.name=${projectDefinition.name}'" + "-X '${projectDefinition.modulePath}/internal/release.mnemonic=${projectDefinition.mnemonic}'" + "-X '${projectDefinition.modulePath}/internal/release.buildDate=${lastModifiedDate}'" + ]; - meta = with nixpkgs.legacyPackages.${system}.lib; { - description = "Bob: The HTML and HTML fragment builder"; - homepage = "https://gitlab.schukai.com/oss/bob"; + +vendorHash = projectDefinition.vendorHash; + proxyVendor = true; + + meta = with system.lib; { + description = projectDefinition.description; + homepage = "https://" + projectDefinition.modulePath; license = licenses.mit; - maintainers = with maintainers; [ "schukai GmbH" ]; - }; + maintainers = with maintainers; ["schukai GmbH"]; + }; + + buildInputs = [pkgs.bash]; + nativeBuildInputs = with system; [pkgs.alejandra pkgs.shellcheck pkgs.shfmt]; + + doCheck = true; + + checkPhase = '' + cd ${sourcePath} + if ! go test -v ./... 2>&1 | cat ; then + echo "Test failed." + exit 1 + fi + + echo "Test passed: '$out' is as expected." + ''; + }; }); diff --git a/hashes.nix b/hashes.nix deleted file mode 100644 index 2c9ec8b6db4162cb3aa5143562e5593bf87ca83f..0000000000000000000000000000000000000000 --- a/hashes.nix +++ /dev/null @@ -1,3 +0,0 @@ -{ - hashValue = "sha256-M4pZBhlt0CrRgAf9DxRUVNmA2sYxDYIccreOQy7qmrk="; -} diff --git a/project.nix b/project.nix new file mode 100644 index 0000000000000000000000000000000000000000..a75540c7129d458294481d0ee7064a69b5b78052 --- /dev/null +++ b/project.nix @@ -0,0 +1,20 @@ +{ + name = "Bob"; + mnemonic = "bob"; + description = "The HTML and HTML fragment builder."; + supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; + compileForSystems = ["linux/arm64" "linux/amd64" "darwin/amd64" "windows/amd64"]; + modulePath = "gitlab.schukai.com/oss/bob"; + + k8s = { + loglevel = "info"; + port = 80; + replicas = 1; + }; + + registry = "docker-registry.schukai.com:443"; + + setup = "done"; + version = "0.1.0"; ## do not change this line; it will be updated automatically + vendorHash = "sha256-M4pZBhlt0CrRgAf9DxRUVNmA2sYxDYIccreOQy7qmrk="; +} \ No newline at end of file diff --git a/source/command.go b/source/command.go index 97fbc52d1f45bb4956d834cf05d31dbcec5deedd..a04d6d4f702cc4172036f8e041ff9fcd22dc509a 100644 --- a/source/command.go +++ b/source/command.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/charmbracelet/log" html2 "gitlab.schukai.com/oss/bob/html" template2 "gitlab.schukai.com/oss/bob/template" "gitlab.schukai.com/oss/bob/types" @@ -15,7 +16,6 @@ import ( type Definition struct { Help struct { } `command:"help" call:"PrintHelp" description:"Prints this help message"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` Template struct { Prepare struct { Input string `short:"i" long:"input" description:"Directory with html files to prepare" required:"true"` @@ -106,9 +106,10 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) { } } - if d.Verbose { - fmt.Printf("Loaded data file %s\n", i) - } + log.Info("Loaded data file " + i) + + // reset all content entries + storage.ResetContent() } toDelete := []string{} @@ -131,9 +132,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) { return nil } - if d.Verbose { - fmt.Printf("Prepare %s\n", path) - } + log.Info("Prepare " + path) key, err := template2.PrepareHtmlFile(path, d.Template.Prepare.Output, storage) if err != nil { @@ -152,9 +151,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) { for _, v := range toDelete { - if d.Verbose { - fmt.Printf("Delete %s\n", v) - } + log.Info("Delete " + v) delete(storage, v) } @@ -175,9 +172,7 @@ func (d *Definition) PrepareTemplate(s *xflags.Settings[Definition]) { s.AddError(err) } - if d.Verbose { - fmt.Printf("Saved data file %s\n", o) - } + log.Info("Saved data file " + o) } diff --git a/source/go.mod b/source/go.mod index 61dd0d07f11391d1ebcf5b46c628c616e249190f..4796b7138a928fdbeb7a9610ea893672ac87085b 100644 --- a/source/go.mod +++ b/source/go.mod @@ -12,7 +12,19 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/charmbracelet/log v0.3.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/volker-schukai/tokenizer v1.0.0 // indirect gitlab.schukai.com/oss/libraries/go/utilities/data.git v0.2.0 // indirect gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.9.2 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.16.0 // indirect ) diff --git a/source/go.sum b/source/go.sum index 3775078da48514fbb1980e1637f506e01480ba82..b91bd4533bdd9ea8d1dfcb7a83470805f8e70aea 100644 --- a/source/go.sum +++ b/source/go.sum @@ -2,10 +2,32 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= +github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -44,6 +66,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -71,7 +95,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/source/html/cut_test.go b/source/html/cut_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4d603d44e31ed4bd4b5dd1f20295a693aeb4acef --- /dev/null +++ b/source/html/cut_test.go @@ -0,0 +1,159 @@ +package html + +import ( + "bytes" + "github.com/andybalholm/cascadia" + "gitlab.schukai.com/oss/bob/types" + "golang.org/x/net/html" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +// Helper function to create a temporary file with content +func createTempFile(content string) (string, error) { + tmpfile, err := ioutil.TempFile("", "example.*.html") + if err != nil { + return "", err + } + + if _, err := tmpfile.Write([]byte(content)); err != nil { + return "", err + } + + if err := tmpfile.Close(); err != nil { + return "", err + } + + return tmpfile.Name(), nil +} + +// TestCutHtml tests the CutHtml function +func TestCutHtml(t *testing.T) { + // Create temporary directory + tempDir, err := ioutil.TempDir("", "testcuthtml") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a temporary source HTML file + sourceHTML := `<div><p class="content">Original content</p></div>` + sourcePath := filepath.Join(tempDir, "source.html") + if err := ioutil.WriteFile(sourcePath, []byte(sourceHTML), 0644); err != nil { + t.Fatalf("Failed to write source HTML file: %v", err) + } + + // Create a SnippetsSpecification in YAML format + spec := types.SnippetsSpecification{ + Snippets: []types.Snippet{ + { + Source: sourcePath, + Destination: sourcePath, + Replacement: []types.ContentReplacement{ + { + Selector: ".content", + Content: `<p class="content">Replaced content</p>`, + }, + }, + }, + }, + } + specBytes, err := yaml.Marshal(spec) + if err != nil { + t.Fatalf("Failed to marshal YAML: %v", err) + } + + // Write the YAML to a temporary file + specPath := filepath.Join(tempDir, "spec.yaml") + if err := ioutil.WriteFile(specPath, specBytes, 0644); err != nil { + t.Fatalf("Failed to write spec YAML file: %v", err) + } + + // Run the CutHtml function + if err := CutHtml(specPath); err != nil { + t.Fatalf("CutHtml failed: %v", err) + } + + // Verify the result + modifiedHTML, err := ioutil.ReadFile(sourcePath) + if err != nil { + t.Fatalf("Failed to read modified HTML file: %v", err) + } + + expectedHTML := `<div><p class="content">Replaced content</p></div>` + if string(modifiedHTML) != expectedHTML { + t.Errorf("Expected HTML to be '%v', but got '%v'", expectedHTML, string(modifiedHTML)) + } +} + +// TestSetAttributes tests the setAttributes function +func TestSetAttributes(t *testing.T) { + // Example HTML node + rawHTML := `<div><p class="old-class">Hello</p></div>` + node, _ := html.Parse(strings.NewReader(rawHTML)) + attrs := []types.Attributes{{Selector: "p", Name: "class", Value: "new-class"}} + + // Perform the attribute setting + err := setAttributes(node, attrs) + if err != nil { + t.Errorf("setAttributes failed: %v", err) + } + + // Check if the attribute was set correctly + query, _ := cascadia.Compile("p") + pNode := query.MatchFirst(node) + if pNode == nil { + t.Errorf("p node not found") + } else if pNode.Attr[0].Val != "new-class" { + t.Errorf("Attribute not set correctly, got: %s, want: new-class", pNode.Attr[0].Val) + } +} + +// TestRemoveAttribute tests the removeAttribute function +func TestRemoveAttribute(t *testing.T) { + // Example attributes + attrs := []html.Attribute{{Key: "class", Val: "old-class"}, {Key: "id", Val: "test-id"}} + + // Remove the 'class' attribute + updatedAttrs := removeAttribute(attrs, "class") + + // Check if the 'class' attribute is removed + for _, attr := range updatedAttrs { + if attr.Key == "class" { + t.Errorf("Attribute 'class' was not removed") + } + } +} + +func TestReplaceNodes(t *testing.T) { + // Example HTML node + rawHTML := `<div><p class="target">Old Content</p><p class="untouched">Don't touch this</p></div>` + node, _ := html.Parse(strings.NewReader(rawHTML)) + replacements := []types.ContentReplacement{ + {Selector: ".target", Content: "<p>New Content</p>"}, + } + + // Perform the replacement + replacedNode, err := replaceNodes(node, replacements) + if err != nil { + t.Fatalf("replaceNodes failed: %v", err) + } + + // Convert the node back to HTML for easy verification + var buf bytes.Buffer + html.Render(&buf, replacedNode) + + replacedHTML := buf.String() + + // Expected HTML after replacement + expectedHTML := `<html><head></head><body><div><p>New Content</p><p class="untouched">Don't touch this</p></div></body></html>` + + // Verify the replacement + if replacedHTML != expectedHTML { + t.Errorf("Expected HTML to be '%v', but got '%v'", expectedHTML, replacedHTML) + } +} diff --git a/source/html/generate.go b/source/html/generate.go index 40c579600618cfc71caf8cd97a4c98b194f057d3..394c4cd8cebf4af9cf9b1358cfc747c8f88cf14a 100644 --- a/source/html/generate.go +++ b/source/html/generate.go @@ -3,6 +3,7 @@ package html import ( "encoding/json" "github.com/andybalholm/cascadia" + "github.com/charmbracelet/log" "gitlab.schukai.com/oss/bob/types" "gitlab.schukai.com/oss/libraries/go/markup/html/engine" "golang.org/x/net/html" @@ -28,8 +29,10 @@ func GenerateFiles(dataPath, templates, out string) error { for name, page := range storage { p := path.Join(templatesDir, name) + log.Info("Generating with template " + p) generatedHtml, err := Generate(page, p) if err != nil { + log.Error("Error " + err.Error() + " while generating " + p) return err } @@ -37,11 +40,15 @@ func GenerateFiles(dataPath, templates, out string) error { dir := path.Dir(outFile) if err := os.MkdirAll(dir, 0755); err != nil { + + log.Error("Error " + err.Error() + " while creating directory " + dir) return err } + log.Info("Writing " + outFile) err = os.WriteFile(outFile, []byte(generatedHtml), 0644) if err != nil { + log.Error("Error " + err.Error() + " while writing " + outFile) return err } @@ -55,10 +62,17 @@ func Generate(data *types.PageData, name string) (string, error) { dataset := make(map[any]any) dataset["lang"] = data.Lang dataset["title"] = data.Title + if data.Title == "" { + log.Warn("No title set for " + name) + } + + log.Info("Generating " + data.Export + " with template " + name) + dataset["meta"] = make(map[any]any) for k, v := range data.Meta { dataset["meta"].(map[any]any)[k] = v } + dataset["images"] = make(map[any]any, 0) for _, v := range data.Images { dataset["images"].(map[any]any)[v.Id] = map[any]any{ @@ -112,11 +126,19 @@ func Generate(data *types.PageData, name string) (string, error) { func doModifications(page *types.PageData, from string) (string, error) { + log.Info("Modifications for " + page.Export) + node, err := html.Parse(strings.NewReader(from)) if err != nil { return "", err } + if len(page.Modifications.Remove) == 0 { + log.Info("No remove modifications set for " + page.Export) + } else { + log.Infof("There are %d remove modifications set for %s", len(page.Modifications.Remove), page.Export) + } + for _, m := range page.Modifications.Remove { selector, err := cascadia.Parse(m) @@ -129,7 +151,12 @@ func doModifications(page *types.PageData, from string) (string, error) { n.Parent.RemoveChild(n) } } + } + if len(page.Modifications.Add) == 0 { + log.Info("No add modifications set for " + page.Export) + } else { + log.Infof("There are %d add modifications set for %s", len(page.Modifications.Add), page.Export) } for _, m := range page.Modifications.Add { @@ -153,6 +180,12 @@ func doModifications(page *types.PageData, from string) (string, error) { } } + if len(page.Modifications.SetAttribute) == 0 { + log.Info("No set attribute modifications set for " + page.Export) + } else { + log.Infof("There are %d set attribute modifications set for %s", len(page.Modifications.SetAttribute), page.Export) + } + for _, m := range page.Modifications.SetAttribute { selector, err := cascadia.Parse(m.Selector) if err != nil { @@ -170,6 +203,8 @@ func doModifications(page *types.PageData, from string) (string, error) { Val: m.Value, }) + log.Info("Set attribute " + m.Name + " to " + m.Value + " for " + page.Export) + n.Attr = attr } } diff --git a/source/template/prepare.go b/source/template/prepare.go index aa75cc21464b5cd0c39e4bf596b03510042e7816..b5d09495103e8074e943225941b17da073b2b7ec 100644 --- a/source/template/prepare.go +++ b/source/template/prepare.go @@ -1,15 +1,18 @@ package template import ( + "bytes" "encoding/json" "fmt" "github.com/andybalholm/cascadia" + "github.com/charmbracelet/log" "gitlab.schukai.com/oss/bob/constants" "gitlab.schukai.com/oss/bob/types" "gitlab.schukai.com/oss/bob/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" "path" + "regexp" "strings" ) @@ -32,11 +35,6 @@ func removeAttribute(attrs []html.Attribute, key string) []html.Attribute { return result } -//func setDataAttribute(node *html.Node, name, attribute, instruction string) { -// node.Attr = removeAttribute(node.Attr, name) -// node.Attr = append(node.Attr, html.Attribute{Key: name, Val: attribute + " " + instruction}) -//} - func setDataAttributesAttribute(node *html.Node, name, attribute, instruction string) { value := util.GetAttribute(node.Attr, attributeAttributes) @@ -164,27 +162,65 @@ func prepareAnchors(node *html.Node, storage *types.PageData) { return } - copyOfAnchors := make([]types.Anchor, len(storage.Anchors)) - copy(copyOfAnchors, storage.Anchors) - storage.Anchors = []types.Anchor{} - for _, n := range list { + logContent := buildLogContent(n, "") + log.Info("Found a node: " + logContent) + title := util.GetAttribute(n.Attr, "title") hreflang := util.GetAttribute(n.Attr, "hreflang") href := util.GetAttribute(n.Attr, "href") - id := util.GetOrCreateReference(n, title+hreflang+href) + k := title + hreflang + href + if k == "" { + log.Warn("The anchor '" + logContent + "' has no title, hreflang and href attribute! Will use the content of the anchor as key.") + k = logContent + } + + id := util.GetOrCreateReference(n, k) if id == "" { - id, _ = util.RandomString(8) - n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey) - n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id}) + id = util.GetNextId() + } + + // check if id is already in use + found := false + sameContent := false + for _, t := range storage.Anchors { + if t.Id == id { + // check if the content is the same + if t.Title == title && t.HrefLang == hreflang && t.Href == href { + sameContent = true + continue + } + found = true + break + } + } + + if sameContent { + log.Info("The ID " + id + " for the anchor '" + logContent + "' is already in use and the content is the same! Will use this ID for the anchor.") + } else if found { + originalId := id + id = util.GetNextId() + log.Warn("We want to use the ID " + originalId + " for the anchor '" + logContent + "', but this ID is already in use! " + + "This could be a problem if you want to use the anchor in a translation. " + + "Please change the anchor or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " + + "Will use now use ID: " + id + " instead.") } setDataAttributesAttribute(n, attributeAttributes, "href", "path:anchors."+id+".href") setDataAttributesAttribute(n, attributeAttributes, "title", "path:anchors."+id+".title") setDataAttributesAttribute(n, attributeAttributes, "hreflang", "path:anchors."+id+".hreflang") + n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey) + n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id}) + + if sameContent { + continue + } + + log.Info("Added anchor: " + id + " to storage") + storage.Anchors = append(storage.Anchors, types.Anchor{ Id: id, Title: title, @@ -194,20 +230,6 @@ func prepareAnchors(node *html.Node, storage *types.PageData) { } - for _, anchor := range copyOfAnchors { - foundIndex := -1 - for index, i := range storage.Images { - if i.Id == anchor.Id { - foundIndex = index - break - } - } - - if foundIndex > -1 { - storage.Anchors[foundIndex] = anchor - } - } - } func prepareImages(node *html.Node, storage *types.PageData) { @@ -222,27 +244,57 @@ func prepareImages(node *html.Node, storage *types.PageData) { return } - copyOfImages := make([]types.Image, len(storage.Images)) - copy(copyOfImages, storage.Images) - storage.Images = []types.Image{} - for _, n := range list { + logContent := buildLogContent(n, "") + log.Info("Found img node: " + logContent) + alt := util.GetAttribute(n.Attr, "alt") title := util.GetAttribute(n.Attr, "title") source := util.GetAttribute(n.Attr, "src") id := util.GetOrCreateReference(n, alt+title+source) if id == "" { - id, _ = util.RandomString(8) - n.Attr = removeAttribute(n.Attr, constants.DataBobReferenceAttributeKey) - n.Attr = append(n.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id}) + id = util.GetNextId() } + // check if id is already in use + found := false + sameContent := false + for _, t := range storage.Images { + if t.Id == id { + // check if the content is the same + if t.Alt == alt && t.Title == title && t.Source == source { + sameContent = true + continue + } + found = true + break + } + } + + if sameContent { + log.Info("The ID " + id + " for the image '" + logContent + "' is already in use and the content is the same! Will use this ID for the image.") + } else if found { + originalId := id + id = util.GetNextId() + log.Warn("We want to use the ID " + originalId + " for the image '" + logContent + "', but this ID is already in use! " + + "This could be a problem if you want to use the image in a translation. " + + "Please change the image or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " + + "Will use now use ID: " + id + " instead.") + } + + 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") setDataAttributesAttribute(n, attributeAttributes, "alt", "path:content."+id+".alt") setDataAttributesAttribute(n, attributeAttributes, "title", "path:content."+id+".title") + if sameContent { + continue + } + storage.Images = append(storage.Images, types.Image{ Id: id, Alt: alt, @@ -252,24 +304,10 @@ func prepareImages(node *html.Node, storage *types.PageData) { } - for _, image := range copyOfImages { - foundIndex := -1 - for index, i := range storage.Images { - if i.Id == image.Id { - foundIndex = index - break - } - } - - if foundIndex > -1 { - storage.Images[foundIndex] = image - } - } - } func prepareTranslationJson(node *html.Node, storage *types.PageData) { - selector, err := cascadia.Parse("script[data-monster-role=translation]") + selector, err := cascadia.Parse("script[data-monster-role=translations]") if err != nil { return } @@ -279,14 +317,29 @@ func prepareTranslationJson(node *html.Node, storage *types.PageData) { return } - copyOfTranslations := make([]types.Translations, len(storage.Translations)) - copy(copyOfTranslations, storage.Translations) - storage.Translations = []types.Translations{} - for _, n := range list { id := util.GetAttribute(n.Attr, constants.DataBobReferenceAttributeKey) - typ := util.GetAttribute(n.Attr, "type") + if id == "" { + id = util.GetNextId() + } + + log.Info("Found json translation: " + id) + + found := false + for _, t := range storage.Translations { + if t.Id == id { + found = true + break + } + } + + if found { + log.Info("The ID " + id + " for the json translation is already in use! If this is not intended, please change the ID in the HTML file. Will use ID: " + id) + continue + } + + typ_ := util.GetAttribute(n.Attr, "type") n.Attr = removeAttribute(n.Attr, attributeReplace) n.Attr = append(n.Attr, html.Attribute{Key: attributeReplace, Val: "path:translations." + id + ".content"}) @@ -306,28 +359,15 @@ func prepareTranslationJson(node *html.Node, storage *types.PageData) { fmt.Println(err) } + log.Info("Added json translation: " + id + " (" + typ_ + ") to storage") storage.Translations = append(storage.Translations, types.Translations{ Id: id, - Type: typ, + Type: typ_, KeyValues: t, }) } - for _, translation := range copyOfTranslations { - foundIndex := -1 - for index, t := range storage.Translations { - if t.Id == translation.Id { - foundIndex = index - break - } - } - - if foundIndex > -1 { - storage.Translations[foundIndex] = translation - } - } - } func prepareTextNodes(node *html.Node, storage *types.PageData) { @@ -342,26 +382,8 @@ func prepareTextNodes(node *html.Node, storage *types.PageData) { return } - copyOfTextNodes := make([]types.Text, len(storage.Text)) - copy(copyOfTextNodes, storage.Text) - storage.Text = []types.Text{} - runNodes(body, storage) - for _, text := range copyOfTextNodes { - foundIndex := -1 - for index, t := range storage.Text { - if t.Id == text.Id { - foundIndex = index - break - } - } - - if foundIndex > -1 { - storage.Text[foundIndex] = text - } - } - } func runNodes(n *html.Node, storage *types.PageData) { @@ -378,15 +400,67 @@ func runNodes(n *html.Node, storage *types.PageData) { } +func renderNode(n *html.Node) string { + var buf bytes.Buffer + html.Render(&buf, n) + return buf.String() +} + +func removeDuplicateWhitespaces(str string) string { + re := regexp.MustCompile(`\s+`) + return re.ReplaceAllString(str, " ") +} + func handleTextNode(n *html.Node, storage *types.PageData) { content := strings.TrimSpace(n.Data) if content == "" { return } + logContent := buildLogContent(n.Parent, content) + log.Info("Found text node: " + logContent) + + if n.Parent != nil { + if n.Parent.Type == html.ElementNode { + if util.HasAttribute(n.Parent.Attr, "data-monster-head") { + return + } + } + } + id, err := util.BuildTextKey(content) if err != nil || id == "" { id = util.GetNextId() + log.Warn("Could not build text key for: '" + logContent + "', using ID: " + id + " instead.") + } else { + log.Info("Create text key: " + id) + } + + // check if id is already in use + found := false + sameContent := false + for _, t := range storage.Text { + if t.Id == id { + // check if the content is the same + if t.Text == content { + sameContent = true + continue + } + found = true + break + } + } + + if sameContent { + log.Info("The ID " + id + " for the text '" + logContent + "' is already in use and the content is the same! Will use this ID for the text.") + } else if found { + originalId := id + id = util.GetNextId() + log.Warn("We want to use the ID " + originalId + " for the text '" + logContent + + "', but this ID is already in use! " + + "This could be a problem if you want to use the text in a translation. " + + "Please change the text or the ID in the HTML file. " + + "Will use now use ID: " + id + " instead.") } parent := n.Parent @@ -404,17 +478,42 @@ func handleTextNode(n *html.Node, storage *types.PageData) { }, }, } + parent.InsertBefore(span, n) parent.RemoveChild(n) span.AppendChild(n) + if sameContent { + return + } + + log.Info("Added text: " + id + " (" + logContent + ") to storage") storage.Text = append(storage.Text, types.Text{ Id: id, Text: content, }) } +func buildLogContent(n *html.Node, content string) string { + logContent := "" + if len(content) == 0 { + logContent = "Struct: " + renderNode(n) + } else if len(content) < 10 { + logContent = "Parent struct for (" + content + "): " + renderNode(n) + + } else { + logContent = content + } + + if len(logContent) > 100 { + logContent = logContent[:100] + "..." + } + + logContent = removeDuplicateWhitespaces(logContent) + return logContent +} + func checkNodes(n *html.Node, storage *types.PageData) { if n.Parent != nil { if n.Parent.Type == html.ElementNode { @@ -450,30 +549,57 @@ func checkMonsterDatatableHead(n *html.Node, storage *types.PageData) { for _, div := range list { head := util.GetAttribute(div.Attr, "data-monster-head") + logContent := buildLogContent(div, head) - id := util.GetAttribute(div.Attr, constants.DataBobReferenceAttributeKey) + id := util.GetOrCreateReference(div, head) if id == "" { + id = util.GetNextId() + log.Warn("Could not build key for: '" + logContent + "', using ID: " + id + " instead.") + } else { + log.Info("Create key: " + id) + } - headID, _ := util.BuildTextKey(head) - if headID == "" { - id = util.GetNextId() - } else { - id = headID + // check if id is already in use + found := false + sameContent := false + for _, t := range storage.Text { + if t.Id == id { + // check if the content is the same + if t.Text == head { + sameContent = true + continue + } + found = true + break } - div.Attr = append(div.Attr, html.Attribute{Key: constants.DataBobReferenceAttributeKey, Val: id}) + } + + if sameContent { + log.Info("The ID " + id + " for the head '" + logContent + "' is already in use and the content is the same! Will use this ID for the head.") + } else if found { + originalId := id + id = util.GetNextId() + log.Warn("We want to use the ID " + originalId + " for the head '" + logContent + + "', but this ID is already in use! " + + "This could be a problem if you want to use the head in a translation. " + + "Please change the head or add the attribute '" + constants.DataBobReferenceAttributeKey + "' to the tag. " + + "Will use now use ID: " + id + " instead.") } 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"}) + if sameContent { + continue + } + + log.Info("Added head: " + id + " (" + logContent + ") to storage") storage.Text = append(storage.Text, types.Text{ Id: id, Text: head, }) - } - } func PrepareHtmlFile(from, to string, storage types.PageDataStorage) (string, error) { diff --git a/source/types/page-data.go b/source/types/page-data.go index c5c7166012c28bb4e674a79e0aede058fb4eee7b..7260de7c510e4f7d23fe5348cd0ca6ac91f29e08 100644 --- a/source/types/page-data.go +++ b/source/types/page-data.go @@ -68,3 +68,20 @@ func NewPageDataStorage() PageDataStorage { return storage } + +func (p *PageDataStorage) ResetContent() { + for _, pageData := range *p { + pageData.ResetContent() + } +} + +func (p *PageData) ResetContent() { + + p.Title = "" + p.Meta = make(map[string]string) + p.Images = make([]Image, 0) + p.Anchors = make([]Anchor, 0) + p.Text = make([]Text, 0) + p.Translations = make([]Translations, 0) + +} diff --git a/source/util/html.go b/source/util/html.go index d206b967618415d1f1384f24e327e70b7ab3fe7d..fabf0eb621778681d055d36a83d98580989f8835 100644 --- a/source/util/html.go +++ b/source/util/html.go @@ -20,6 +20,16 @@ func LoadHtml(p string) (*html.Node, error) { } +func HasAttribute(attrs []html.Attribute, key string) bool { + for _, attr := range attrs { + if attr.Key == key { + return true + } + } + + return false +} + func GetAttribute(attrs []html.Attribute, key string) string { for _, attr := range attrs { if attr.Key == key { diff --git a/source/util/id.go b/source/util/id.go index 74a36ab4102d89049bf0b64687e486d1585e5ae5..6c5a6d12ee9218404be26744c03a6cd4c6e4ea4c 100644 --- a/source/util/id.go +++ b/source/util/id.go @@ -67,6 +67,10 @@ func BuildTextKey(txt string) (string, error) { } txt = strings.TrimSpace(txt) + + re := regexp.MustCompile(`([a-z])([A-Z])`) + txt = re.ReplaceAllString(txt, "${1}-${2}") + txt = strings.ToLower(txt) txt = strings.ReplaceAll(txt, keyDelimiter, tempStringMask) diff --git a/source/util/id_test.go b/source/util/id_test.go new file mode 100644 index 0000000000000000000000000000000000000000..229fb4d490e543b9f10c4d57bf606f4002f82399 --- /dev/null +++ b/source/util/id_test.go @@ -0,0 +1,93 @@ +package util + +import ( + "regexp" + "strconv" + "testing" +) + +func TestNewSecret(t *testing.T) { + secret, err := NewSecret() + if err != nil { + t.Errorf("Error creating new secret: %v", err) + } + if len(secret) == 0 { + t.Error("Generated secret is empty") + } +} + +func TestNewSecretWithLength(t *testing.T) { + length := 10 + secret, err := NewSecretWithLength(length) + if err != nil { + t.Errorf("Error creating new secret with length %d: %v", length, err) + } + if len(secret) != length { + t.Errorf("Generated secret length expected to be %d, got %d", length, len(secret)) + } +} + +func TestRandomString(t *testing.T) { + length := 10 + str, err := RandomString(length) + if err != nil { + t.Errorf("Error generating random string: %v", err) + } + if len(str) != length { + t.Errorf("Random string length expected to be %d, got %d", length, len(str)) + } +} + +func TestCryptID(t *testing.T) { + input := "testString" + length := 8 + encrypted, err := cryptID(input, length) + if err != nil { + t.Errorf("Error encrypting string: %v", err) + } + if len(encrypted) != length { + t.Errorf("Encrypted string length expected to be %d, got %d", length, len(encrypted)) + } +} + +func TestHashString(t *testing.T) { + input := "testString" + hashed := hashString(input) + if _, err := strconv.Atoi(hashed); err != nil { + t.Errorf("Hashed string is not a valid number: %s", hashed) + } +} + +func TestBuildTextKey(t *testing.T) { + tests := []struct { + input string + expectedRegex string + }{ + {"Test Key", "^test-key$"}, + {"", "^$"}, + {"ThisIsALongTextKeyThatExceedsTheLimit", "^this-is-along-text-k-[0-9]+$"}, + {"MixedCaseInput", "^mixed-case-input$"}, + {"With Spaces", "^with-spaces$"}, + {"123Numbers456", "^123numbers456$"}, + {"Special#Characters!", "^special-characters$"}, + {"Short", "^short$"}, + {"LongerThanTwentyCharactersInputString", "^longer-than-twenty-c-[0-9]+$"}, + {"-StartsInDash", "^starts-in-dash$"}, + {"EndsInDash-", "^ends-in-dash$"}, + {"With--Multiple--Dashes", "^with-multiple-dashes$"}, + {"EndsInDash-", "^ends-in-dash$"}, + {"-StartsInDash", "^starts-in-dash$"}, + {"Emojis🚀AreCool", "^emojis-are-cool$"}, + } + + for _, test := range tests { + output, err := BuildTextKey(test.input) + if err != nil { + t.Errorf("Error building text key for input '%s': %v", test.input, err) + } + matched, _ := regexp.MatchString(test.expectedRegex, output) + if !matched { + t.Errorf("Output '%s' does not match expected regex '%s' for input '%s'", output, test.expectedRegex, test.input) + } + } +} diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 0000000000000000000000000000000000000000..968b9bf3830426c33c6a9a4012e1e7d073ecf421 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,34 @@ +# One CLI to format the code tree - https://github.com/numtide/treefmt + +[formatter.nix] +command = "nixfmt" +options = [] +includes = [ "*.nix" ] +excludes = [] +[formatter.go] +command = "gofmt" +options = [] +includes = [ "*.go" ] +excludes = [] +[formatter.shell] +command = "shfmt" +options = [ "-i", "2", "-ci" ] +includes = [ "*.sh" ] +excludes = [] +[formatter.yaml] +command = "yq" +options = [ "eval", "-P" ] +includes = [ "*.yaml", "*.yml" ] +excludes = [] +[formatter.json] +command = "jq" +options = [ "." ] +includes = [ "*.json" ] +excludes = [] +[formatter.toml] +command = "tomlfmt" +options = [] +includes = [ "*.toml" ] +excludes = [] + +