'
+ }
+
+ $("#tx-sender-history ul").prepend(li)
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Connection / Setup
+ //////////////////////////////////////////////////////////////////////////////
+
+ const FAUCET_URL = "https://faucet.altnet.rippletest.net/accounts"
+ const TESTNET_URL = "wss://s.altnet.rippletest.net:51233"
+
+ let connection_ready = false
+
+ let sending_address
+ let sending_secret
+ let xrp_balance
+
+
+ console.debug("Getting a sending address from the faucet...")
+
+ faucet_response = function(data) {
+ sending_address = data.account.address
+ sending_secret = data.account.secret
+ xrp_balance = Number(data.balance) // Faucet only delivers ~10,000 XRP,
+ // so this won't go over JavaScript's
+ // 64-bit double precision
+
+ $("#balance-item").text(xrp_balance)
+ $(".sending-address-item").text(sending_address)
+ }
+
+ $.ajax({
+ url: FAUCET_URL,
+ type: 'POST',
+ dataType: 'json',
+ success: faucet_response,
+ error: function() {
+ errorNotif("There was an error with the XRP Ledger Test Net Faucet. Reload this page to try again.")
+ }
+ })
+
+ api = new ripple.RippleAPI({server: TESTNET_URL})
+ api.on('connected', () => {
+ connection_ready = true
+ $("#connection-status-item").text("Connected")
+ $("#connection-status-item").removeClass("disabled").addClass("active")
+ })
+ api.on('disconnected', (code) => {
+ connection_ready = false
+ $("#connection-status-item").text("Not connected")
+ $("#connection-status-item").removeClass("active").addClass("disabled")
+ })
+ console.log("Connecting to Test Net WebSocket...")
+ api.connect()
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Generic Transaction Submission
+ //////////////////////////////////////////////////////////////////////////////
+
+ // Helper function for await-able timeouts
+ function timeout(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ const INTERVAL = 1000 // milliseconds to wait for new ledger versions
+ async function verify_transaction(hash, options) {
+ try {
+ data = await api.getTransaction(hash, options)
+ return data
+ } catch(error) {
+ /* If transaction not in latest validated ledger,
+ try again until max ledger hit */
+ if (error instanceof api.errors.PendingLedgerVersionError) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => verify_transaction(hash, options)
+ .then(resolve, reject), INTERVAL)
+ })
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ async function update_xrp_balance() {
+ balances = await api.getBalances(sending_address, {currency: "XRP"})
+ $("#balance-item").text(balances[0].value)
+ }
+
+ async function submit_and_verify(tx_object, use_secret, silent) {
+ if (use_secret === undefined) {
+ use_secret = sending_secret
+ }
+ try {
+ // Auto-fill fields like Fee and Sequence
+ prepared = await api.prepareTransaction(tx_object)
+ console.debug("Prepared:", prepared)
+ } catch(error) {
+ console.log(error)
+ if (!silent) {
+ errorNotif("Error preparing tx: "+error)
+ }
+ return
+ }
+
+ // Determine first and last ledger the tx could be validated in *BEFORE*
+ // signing it.
+ const options = {
+ minLedgerVersion: (await api.getLedger()).ledgerVersion,
+ maxLedgerVersion: prepared.instructions.maxLedgerVersion
+ }
+
+ let sign_response
+ try {
+ // Sign, submit
+ sign_response = api.sign(prepared.txJSON, use_secret)
+ await api.submit(sign_response.signedTransaction)
+ } catch (error) {
+ console.log(error)
+ if (!silent) {
+ errorNotif("Error signing & submitting "+tx_object.TransactionType+" tx: "+error)
+ }
+ return
+ }
+
+ // Wait for tx to be in a validated ledger or to expire
+ try {
+ const data = await verify_transaction(sign_response.id, options)
+ const final_result = data.outcome.result
+ // Future feature: output should link to a TestNet tx lookup/explainer
+ if (final_result === "tesSUCCESS") {
+ if (!silent) {
+ successNotif(tx_object.TransactionType+" tx succeeded (hash: "+sign_response.id+")")
+ logTx(tx_object.TransactionType, sign_response.id, final_result)
+ }
+ } else {
+ if (!silent) {
+ errorNotif(tx_object.TransactionType+" tx failed w/ code "+final_result+
+ " (hash: "+sign_response.id+")")
+ logTx(tx_object.TransactionType, sign_response.id, final_result)
+ }
+ }
+ update_xrp_balance()
+ return data
+ } catch(error) {
+ console.log(error)
+ if (!silent) {
+ errorNotif("Error submitting "+tx_object.TransactionType+" tx: "+error)
+ }
+ }
+
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Issuer Setup for Partial Payments
+ // (Partial payments must involve at least one issued currency, so we set up
+ // an issuer for a fake currency to ripple through.)
+ //////////////////////////////////////////////////////////////////////////////
+
+ let pp_issuer_address
+ let pp_sending_currency = "BAR"
+ async function set_up_for_partial_payments() {
+ while (!connection_ready) {
+ console.debug("... waiting for connection before doing partial payment setup")
+ await timeout(200)
+ }
+ console.debug("Starting partial payment setup...")
+ $("#pp_progress .progress-bar").addClass("progress-bar-animated")
+ // 1. Get a funded address to use as issuer
+ let pp_issuer_secret
+ try {
+ const faucet_response = await ($.ajax({
+ url: FAUCET_URL,
+ type: 'POST',
+ dataType: 'json'
+ }))
+ pp_issuer_address = faucet_response.account.address
+ pp_issuer_secret = faucet_response.account.secret
+ } catch(error) {
+ console.log("Error getting issuer address for partial payments:", error)
+ return
+ }
+ $("#pp_progress .progress-bar").width("20%")
+
+ // 2. Set DefaultRipple on issuer
+ let resp = await submit_and_verify({
+ TransactionType: "AccountSet",
+ Account: pp_issuer_address,
+ SetFlag: 8
+ }, pp_issuer_secret, true)
+ if (resp === undefined) {
+ console.log("Couldn't set DefaultRipple for partial payment issuer")
+ return
+ }
+ $("#pp_progress .progress-bar").width("40%")
+
+ // 3. Make a trust line from sending address to issuer
+ resp = await submit_and_verify({
+ TransactionType: "TrustSet",
+ Account: sending_address,
+ LimitAmount: {
+ currency: pp_sending_currency,
+ value: "1000000000", // arbitrarily, 1 billion fake currency
+ issuer: pp_issuer_address
+ }
+ }, sending_secret, true)
+ if (resp === undefined) {
+ console.log("Error making trust line to partial payment issuer")
+ return
+ }
+ $("#pp_progress .progress-bar").width("60%")
+
+ // 4. Issue fake currency to main sending address
+ resp = await submit_and_verify({
+ TransactionType: "Payment",
+ Account: pp_issuer_address,
+ Destination: sending_address,
+ Amount: {
+ currency: pp_sending_currency,
+ value: "1000000000",
+ issuer: pp_issuer_address
+ }
+ }, pp_issuer_secret, true)
+ if (resp === undefined) {
+ console.log("Error sending fake currency from partial payment issuer")
+ return
+ }
+ $("#pp_progress .progress-bar").width("80%")
+
+ // 5. Place offer to buy issued currency for XRP
+ // When sending the partial payment, the sender consumes their own offer (!)
+ // so they end up paying themselves issued currency then delivering XRP.
+ resp = await submit_and_verify({
+ TransactionType: "OfferCreate",
+ Account: sending_address,
+ TakerGets: "1000000000000000", // 1 billion XRP
+ TakerPays: {
+ currency: pp_sending_currency,
+ value: "1000000000",
+ issuer: pp_issuer_address
+ }
+ }, sending_secret, true)
+ if (resp === undefined) {
+ console.log("Error placing order to enable partial payments")
+ return
+ }
+ $("#pp_progress .progress-bar").width("100%").removeClass("progress-bar-animated")
+ $("#pp_progress").hide()
+
+ // Done. Enable "Send Partial Payment" button
+ console.log("Done getting ready to send partial payments.")
+ $("#send_partial_payment button").prop("disabled",false)
+ $("#send_partial_payment button").attr("title", "")
+ }
+ set_up_for_partial_payments()
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Button Handlers
+ //////////////////////////////////////////////////////////////////////////////
+
+ // 1. Send XRP Payment Handler -------------------------------------------
+ async function on_click_send_xrp_payment(event) {
+ const destination_address = $("#destination_address").val()
+ const xrp_drops_input = $("#send_xrp_payment_amount").val()
+ $("#send_xrp_payment .loader").show()
+ $("#send_xrp_payment button").attr("disabled","disabled")
+ await submit_and_verify({
+ TransactionType: "Payment",
+ Account: sending_address,
+ Destination: destination_address,
+ Amount: xrp_drops_input
+ })
+ $("#send_xrp_payment .loader").hide()
+ $("#send_xrp_payment button").attr("disabled",false)
+
+ }
+ $("#send_xrp_payment button").click(on_click_send_xrp_payment)
+
+ // 2. Send Partial Payment Handler ---------------------------------------
+ async function on_click_send_partial_payment(event) {
+ const destination_address = $("#destination_address").val()
+ $("#send_partial_payment .loader").show()
+ $("#send_partial_payment button").attr("disabled","disabled")
+
+ // const path_find_result = await api.request("ripple_path_find", {
+ // source_account: sending_address,
+ // destination_account: destination_address,
+ // destination_amount: "-1", // as much XRP as possible
+ // source_currencies: [{currency: pp_sending_currency, issuer: pp_issuer_address}]
+ // })
+ // console.log("Path find result:", path_find_result)
+ // use_path = path_find_result.alternatives[0].paths_computed
+
+ await submit_and_verify({
+ TransactionType: "Payment",
+ Account: sending_address,
+ Destination: destination_address,
+ Amount: "1000000000000000", // 1 billion XRP
+ SendMax: {
+ value: (Math.random()*.01).toPrecision(15), // random very small amount
+ currency: pp_sending_currency,
+ issuer: pp_issuer_address
+ },
+ Flags: api.txFlags.Payment.PartialPayment | api.txFlags.Universal.FullyCanonicalSig
+ })
+ $("#send_partial_payment .loader").hide()
+ $("#send_partial_payment button").attr("disabled",false)
+ }
+ $("#send_partial_payment button").click(on_click_send_partial_payment)
+
+
+ // 3. Create Escrow Handler ----------------------------------------------
+ async function on_click_create_escrow(event) {
+ const destination_address = $("#destination_address").val()
+ const duration_seconds_txt = $("#create_escrow_duration_seconds").val()
+ const release_auto = $("#create_escrow_release_automatically").prop("checked")
+
+ const duration_seconds = parseInt(duration_seconds_txt, 10)
+ if (duration_seconds === NaN || duration_seconds < 1) {
+ errorNotif("Error: Escrow duration must be a positive number of seconds")
+ return
+ }
+ const finish_after = api.iso8601ToRippleTime(Date()) + duration_seconds
+
+ $("#create_escrow .loader").show()
+ $("#create_escrow button").attr("disabled","disabled")
+ const escrowcreate_tx_data = await submit_and_verify({
+ TransactionType: "EscrowCreate",
+ Account: sending_address,
+ Destination: destination_address,
+ Amount: "1000000",
+ FinishAfter: finish_after
+ })
+
+ if (release_auto) {
+ // Wait until there's a ledger with a close time > FinishAfter
+ // to submit the EscrowFinish
+ $("#escrow_progress .progress-bar").width("0%").addClass("progress-bar-animated")
+ $("#escrow_progress").show()
+ let seconds_left
+ let pct_done
+ let latestCloseTimeRipple
+ while (true) {
+ seconds_left = (finish_after - api.iso8601ToRippleTime(Date()))
+ pct_done = Math.min(99, Math.max(0, (1-(seconds_left / duration_seconds)) * 100))
+ $("#escrow_progress .progress-bar").width(pct_done+"%")
+ if (seconds_left <= 0) {
+ // System time has advanced past FinishAfter. But is there a new
+ // enough validated ledger?
+ latestCloseTimeRipple = api.iso8601ToRippleTime((await api.getLedger()).closeTime)
+ if (latestCloseTimeRipple > finish_after) {
+ $("#escrow_progress .progress-bar").width("100%").removeClass("progress-bar-animated")
+ break
+ }
+ }
+ // Update the progress bar & check again in 1 second.
+ await timeout(1000)
+ }
+ $("#escrow_progress").hide()
+
+ // Now submit the EscrowFinish
+ // Future feature: submit from a different sender, just to prove that
+ // escrows can be finished by a third party
+ await submit_and_verify({
+ Account: sending_address,
+ TransactionType: "EscrowFinish",
+ Owner: sending_address,
+ OfferSequence: escrowcreate_tx_data.sequence
+ })
+ }
+ $("#create_escrow .loader").hide()
+ $("#create_escrow button").attr("disabled",false)
+ }
+ $("#create_escrow button").click(on_click_create_escrow)
+
+ // 4. Create Payment Channel Handler -------------------------------------
+ async function on_click_create_payment_channel(event) {
+ const destination_address = $("#destination_address").val()
+ const xrp_drops_input = $("#create_payment_channel_amount").val()
+ const pubkey = api.deriveKeypair(sending_secret).publicKey
+ $("#create_payment_channel .loader").show()
+ $("#create_payment_channel button").attr("disabled","disabled")
+ await submit_and_verify({
+ TransactionType: "PaymentChannelCreate",
+ Account: sending_address,
+ Destination: destination_address,
+ Amount: xrp_drops_input,
+ SettleDelay: 30,
+ PublicKey: pubkey
+ })
+ $("#create_payment_channel .loader").hide()
+ $("#create_payment_channel button").attr("disabled",false)
+
+ // Future feature: figure out channel ID and enable a button that creates
+ // valid claims for the given payment channel to help test redeeming
+ }
+ $("#create_payment_channel button").click(on_click_create_payment_channel)
+
+
+ // 5. Send Issued Currency Handler ---------------------------------------
+ async function on_click_send_issued_currency(event) {
+ const destination_address = $("#destination_address").val()
+ const issue_amount = $("#send_issued_currency_amount").val()
+ const issue_code = $("#send_issued_currency_code").text()
+ $("#send_issued_currency .loader").show()
+ $("#send_issued_currency button").attr("disabled","disabled")
+ // Future feature: cross-currency sending with paths?
+ await submit_and_verify({
+ TransactionType: "Payment",
+ Account: sending_address,
+ Destination: destination_address,
+ Amount: {
+ "currency": issue_code,
+ "value": issue_amount,
+ "issuer": sending_address
+ }
+ })
+ $("#send_issued_currency .loader").hide()
+ $("#send_issued_currency button").attr("disabled",false)
+ }
+ $("#send_issued_currency button").click(on_click_send_issued_currency)
+
+ // 6. Trust For Handler
+ async function on_trust_for(event) {
+ const destination_address = $("#destination_address").val()
+ const trust_limit = $("#trust_for_amount").val()
+ const trust_currency_code = $("#trust_for_currency_code").text()
+ $("#trust_for .loader").show()
+ $("#trust_for button").attr("disabled","disabled")
+ await submit_and_verify({
+ TransactionType: "TrustSet",
+ Account: sending_address,
+ LimitAmount: {
+ currency: trust_currency_code,
+ value: trust_limit,
+ issuer: destination_address
+ }
+ })
+ $("#trust_for .loader").hide()
+ $("#trust_for button").attr("disabled",false)
+ }
+ $("#trust_for button").click(on_trust_for)
+
+}
+
+
+$(document).ready( function() {
+ set_up_tx_sender()
+} )
diff --git a/assets/vendor/bootstrap-growl.jquery.js b/assets/vendor/bootstrap-growl.jquery.js
new file mode 100644
index 0000000000..9f4289f7f1
--- /dev/null
+++ b/assets/vendor/bootstrap-growl.jquery.js
@@ -0,0 +1,101 @@
+/*
+The MIT License
+
+Copyright (c) Nick Larson, http://github.com/ifightcrime
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+(function() {
+ var $;
+
+ $ = jQuery;
+
+ $.bootstrapGrowl = function(message, options) {
+ var $alert, css, offsetAmount;
+ options = $.extend({}, $.bootstrapGrowl.default_options, options);
+ $alert = $("
");
+ $alert.attr("class", "bootstrap-growl alert");
+ if (options.type) {
+ $alert.addClass("alert-" + options.type);
+ }
+ if (options.allow_dismiss) {
+ $alert.addClass("alert-dismissible");
+ $alert.append("");
+ }
+ $alert.append(message);
+ if (options.top_offset) {
+ options.offset = {
+ from: "top",
+ amount: options.top_offset
+ };
+ }
+ offsetAmount = options.offset.amount;
+ $(".bootstrap-growl").each(function() {
+ return offsetAmount = Math.max(offsetAmount, parseInt($(this).css(options.offset.from)) + $(this).outerHeight() + options.stackup_spacing);
+ });
+ css = {
+ "position": (options.ele === "body" ? "fixed" : "absolute"),
+ "margin": 0,
+ "z-index": "9999",
+ "display": "none"
+ };
+ css[options.offset.from] = offsetAmount + "px";
+ $alert.css(css);
+ if (options.width !== "auto") {
+ $alert.css("width", options.width + "px");
+ }
+ $(options.ele).append($alert);
+ switch (options.align) {
+ case "center":
+ $alert.css({
+ "left": "50%",
+ "margin-left": "-" + ($alert.outerWidth() / 2) + "px"
+ });
+ break;
+ case "left":
+ $alert.css("left", "20px");
+ break;
+ default:
+ $alert.css("right", "20px");
+ }
+ $alert.fadeIn();
+ if (options.delay > 0) {
+ $alert.delay(options.delay).fadeOut(function() {
+ return $(this).alert("close");
+ });
+ }
+ return $alert;
+ };
+
+ $.bootstrapGrowl.default_options = {
+ ele: "body",
+ type: "info",
+ offset: {
+ from: "top",
+ amount: 20
+ },
+ align: "right",
+ width: 250,
+ delay: 4000,
+ allow_dismiss: true,
+ stackup_spacing: 10
+ };
+
+}).call(this);
diff --git a/content/dev-tools/dev-tools.md b/content/dev-tools/dev-tools.md
index e7933edecd..bb1a78120d 100644
--- a/content/dev-tools/dev-tools.md
+++ b/content/dev-tools/dev-tools.md
@@ -5,24 +5,29 @@ Ripple provides a set of developer tools to help you test, explore, and validate
* **[XRP Ledger Lookup Tool](xrp-ledger-rpc-tool.html)**
- Use this JSON-RPC-based debugging tool to print raw information about a XRP Ledger account, transaction, or ledger.
-
-* **[XRP Ledger Test Net Faucet](xrp-test-net-faucet.html)**
-
- Use the WebSocket and JSON-RPC Test Net endpoints to test software built on the XRP Ledger without using real funds. Generate Test Net credentials and funds for testing purposes. Test net ledger and balances are reset on a regular basis.
-
+ Use this JSON-RPC-based debugging tool to print raw information about a XRP Ledger account, transaction, or ledger.
* **[rippled API WebSocket Tool](websocket-api-tool.html)**
- Need to see the rippled API in action ASAP? Use this tool to send prepopulated sample requests and get responses. No setup required.
-
+ Need to see the rippled API in action ASAP? Use this tool to send sample requests and get responses. No setup required.
* **[Data API v2 Tool](data-api-v2-tool.html)**
- Need to see the Data API v2 in action ASAP? Use this tool to send prepopulated sample requests and get responses. No setup required.
+ Need to see the Data API v2 in action ASAP? Use this tool to send prepopulated sample requests and get responses. No setup required.
-* **[rippled.txt Validator](ripple-txt-validator.html)**
+* **[XRP Ledger Test Net Faucet](xrp-test-net-faucet.html)**
+
+ Use the WebSocket and JSON-RPC Test Net endpoints to test software built on the XRP Ledger without using real funds. Generate Test Net credentials and funds for testing purposes. Test Net ledger and balances are reset on a regular basis.
+
+* **[ripple.txt Validator](ripple-txt-validator.html)**
Use this tool to verify that your `ripple.txt` is syntactically correct and deployed properly.
+ **Warning:** The `ripple.txt` file definition has been deprecated. Use an [xrp-ledger.toml file](xrp-ledger-toml.html) instead.
+
+* **[Transaction Sender](tx-sender.html)**
+
+ Test how your code handles various XRP Ledger transactions by sending them over the Test Net to the address of your choice.
+
+
Have an idea for a tool not provided here? [Contact us >](mailto:docs@ripple.com)
diff --git a/dactyl-config.yml b/dactyl-config.yml
index 6124c67326..1630be7e72 100644
--- a/dactyl-config.yml
+++ b/dactyl-config.yml
@@ -2870,12 +2870,21 @@ pages:
# Dev Tools --------------------------------------------------------------------
- md: dev-tools/dev-tools.md
+ html: dev-tools.html
funnel: Dev Tools
filters:
- buttonize
targets:
- local
+ - name: Dev Tools # Redirect page for old broken URL
+ html: dev-tools-dev-tools.html
+ template: template-redirect.html
+ redirect_url: dev-tools.html
+ funnel: Dev Tools
+ targets:
+ - local
+
- name: RPC Tool
funnel: Dev Tools
html: xrp-ledger-rpc-tool.html
@@ -2915,6 +2924,13 @@ pages:
- local
template: template-test-net.html
+ - name: Transaction Sender
+ funnel: Dev Tools
+ html: tx-sender.html
+ targets:
+ - local
+ template: template-tx-sender.html
+
# News -------------------------------------------------------------------------
- md: news/news.md
diff --git a/tool/template-page-children.html b/tool/template-page-children.html
index d9fbef90f7..ccabb35087 100644
--- a/tool/template-page-children.html
+++ b/tool/template-page-children.html
@@ -39,6 +39,7 @@
{% set printed_next_levels = [] %}
{% for onepage in thosepages %}
{% if onepage == parent %}{# pass #}
+ {% elif onepage.template == "template-redirect.html" %}{# don't list redirects #}
{% elif next_level_field == None or (onepage[next_level_field] is undefined and next_level_field != "supercategory") %}
{# direct child, print it! #}
{{onepage.name}}{% if show_blurbs and onepage.blurb is defined and indent_level == 1%}
{{onepage.blurb}}
{% endif %}
diff --git a/tool/template-redirect.html b/tool/template-redirect.html
new file mode 100644
index 0000000000..6a00edcee4
--- /dev/null
+++ b/tool/template-redirect.html
@@ -0,0 +1,14 @@
+{% extends "template-base.html" %}
+{% block head %}
+
+
+{% endblock %}
+
+
+{% block main %}
+
+
{% for page in funnelpages %}
{% if loop.index == 1 %}{# Skip the first element since it's linked by the funnel header #}
+ {% elif page.template == "template-redirect.html" %}{# skip redirects #}
{% elif page == currentpage %}
{% else %}
@@ -82,33 +83,34 @@
{% set printed_subcategories = [] %}
{% for page in catpages %}
{% if loop.index != 1 %}{# Skip the first element since it's linked by the category header #}
- {% if page.subcategory is undefined %}
- {% if page == currentpage %}
-
{% set category_members = supercatpages|selectattr('category', 'defined_and_equalto', subpage.category)|list %}
@@ -204,33 +207,34 @@
{% set printed_subcategories = [] %}
{% for page in catpages %}
{% if loop.index != 1 %}{# Skip the first element since it's linked by the category header #}
- {% if page.subcategory is undefined %}
- {% if page == currentpage %}
-