Amm0's Forum Plugins Tests — ` ` `routeros & [graphviz] diagrams & colors

To even get syntax coloring, you see you use block with
```routeros
starting on a line
``` three backticks closes the RouterOS code block

Some various tests of the syntax coloring in this forum which apparently uses [highlight.js] and presumably this plugin:

Simple Examples

/ip/address add interface=ether1 address=169.254.1.1/16
/ip address add interface=ether1 address=169.254.1.1/16

More Complex

:global myname "forum"
/system/identity set name=$myname
:onerror err in={
    /ip/address add interface=ether1 address=169.254.1.1/16
} do={
    :put "address already assigned for $myname"
}
/routing
bgp
evpn print 

{
   :local mybfd [:serialize to=dsv decimator=, options=dsv.remap [bfd print as-value]]
   /file add name="bfd.csv" contents=$mybfd
}

JavaScript implementation of syntax coloring.

From GitHub highlight.js/src/languages/routeros.js at 98b649fa2fd3c318b26b01233d9893e04f669c0a · highlightjs/highlight.js · GitHub

/*
Language: MikroTik RouterOS script
Author: Ivan Dementev <ivan_div@mail.ru>
Description: Scripting host provides a way to automate some router maintenance tasks by means of executing user-defined scripts bounded to some event occurrence
Website: https://wiki.mikrotik.com/wiki/Manual:Scripting
Category: scripting
*/

// Colors from RouterOS terminal:
//   green        - #0E9A00
//   teal         - #0C9A9A
//   purple       - #99069A
//   light-brown  - #9A9900

export default function(hljs) {
  const STATEMENTS = 'foreach do while for if from to step else on-error and or not in';

  // Global commands: Every global command should start with ":" token, otherwise it will be treated as variable.
  const GLOBAL_COMMANDS = 'global local beep delay put len typeof pick log time set find environment terminal error execute parse resolve toarray tobool toid toip toip6 tonum tostr totime';

  // Common commands: Following commands available from most sub-menus:
  const COMMON_COMMANDS = 'add remove enable disable set get print export edit find run debug error info warning';

  const LITERALS = 'true false yes no nothing nil null';

  const OBJECTS = 'traffic-flow traffic-generator firewall scheduler aaa accounting address-list address align area bandwidth-server bfd bgp bridge client clock community config connection console customer default dhcp-client dhcp-server discovery dns e-mail ethernet filter firmware gps graphing group hardware health hotspot identity igmp-proxy incoming instance interface ip ipsec ipv6 irq l2tp-server lcd ldp logging mac-server mac-winbox mangle manual mirror mme mpls nat nd neighbor network note ntp ospf ospf-v3 ovpn-server page peer pim ping policy pool port ppp pppoe-client pptp-server prefix profile proposal proxy queue radius resource rip ripng route routing screen script security-profiles server service service-port settings shares smb sms sniffer snmp snooper socks sstp-server system tool tracking type upgrade upnp user-manager users user vlan secret vrrp watchdog web-access wireless pptp pppoe lan wan layer7-protocol lease simple raw';

  // print parameters
  // Several parameters are available for print command:
  // ToDo: var PARAMETERS_PRINT = 'append as-value brief detail count-only file follow follow-only from interval terse value-list without-paging where info';
  // ToDo: var OPERATORS = '&& and ! not || or in ~ ^ & << >> + - * /';
  // ToDo: var TYPES = 'num number bool boolean str string ip ip6-prefix id time array';
  // ToDo: The following tokens serve as delimiters in the grammar: ()  []  {}  :   ;   $   /

  const VAR_PREFIX = 'global local set for foreach';

  const VAR = {
    className: 'variable',
    variants: [
      { begin: /\$[\w\d#@][\w\d_]*/ },
      { begin: /\$\{(.*?)\}/ }
    ]
  };

  const QUOTE_STRING = {
    className: 'string',
    begin: /"/,
    end: /"/,
    contains: [
      hljs.BACKSLASH_ESCAPE,
      VAR,
      {
        className: 'variable',
        begin: /\$\(/,
        end: /\)/,
        contains: [ hljs.BACKSLASH_ESCAPE ]
      }
    ]
  };

  const APOS_STRING = {
    className: 'string',
    begin: /'/,
    end: /'/
  };

  const IPADDR = '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b';
  const IPADDR_wBITMASK = IPADDR + '/(3[0-2]|[1-2][0-9]|\\d)';

  return {
    name: 'MikroTik RouterOS script',
    aliases: [ 'mikrotik' ],
    case_insensitive: true,
    keywords: {
      $pattern: /:?[\w-]+/,
      literal: LITERALS,
      keyword: STATEMENTS + ' :' + STATEMENTS.split(' ').join(' :') + ' :' + GLOBAL_COMMANDS.split(' ').join(' :')
    },
    contains: [
      { // illegal syntax
        variants: [
          { // -- comment
            begin: /\/\*/,
            end: /\*\//
          },
          { // Stan comment
            begin: /\/\//,
            end: /$/
          },
          { // HTML tags
            begin: /<\//,
            end: />/
          }
        ],
        illegal: /./
      },
      hljs.COMMENT('^#', '$'),
      QUOTE_STRING,
      APOS_STRING,
      VAR,
      // attribute=value
      {
        // > is to avoid matches with => in other grammars
        begin: /[\w-]+=([^\s{}[\]()>]+)/,
        relevance: 0,
        returnBegin: true,
        contains: [
          {
            className: 'attribute',
            begin: /[^=]+/
          },
          {
            begin: /=/,
            endsWithParent: true,
            relevance: 0,
            contains: [
              QUOTE_STRING,
              APOS_STRING,
              VAR,
              {
                className: 'literal',
                begin: '\\b(' + LITERALS.split(' ').join('|') + ')\\b'
              },
              {
                // Do not format unclassified values. Needed to exclude highlighting of values as built_in.
                begin: /("[^"]*"|[^\s{}[\]]+)/ }
              /*
              {
                // IPv4 addresses and subnets
                className: 'number',
                variants: [
                  {begin: IPADDR_wBITMASK+'(,'+IPADDR_wBITMASK+')*'}, //192.168.0.0/24,1.2.3.0/24
                  {begin: IPADDR+'-'+IPADDR},       // 192.168.0.1-192.168.0.3
                  {begin: IPADDR+'(,'+IPADDR+')*'}, // 192.168.0.1,192.168.0.34,192.168.24.1,192.168.0.1
                ]
              },
              {
                // MAC addresses and DHCP Client IDs
                className: 'number',
                begin: /\b(1:)?([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]){1,2}\b/,
              },
              */
            ]
          }
        ]
      },
      {
        // HEX values
        className: 'number',
        begin: /\*[0-9a-fA-F]+/
      },
      {
        begin: '\\b(' + COMMON_COMMANDS.split(' ').join('|') + ')([\\s[(\\]|])',
        returnBegin: true,
        contains: [
          {
            className: 'built_in', // 'function',
            begin: /\w+/
          }
        ]
      },
      {
        className: 'built_in',
        variants: [
          { begin: '(\\.\\./|/|\\s)((' + OBJECTS.split(' ').join('|') + ');?\\s)+' },
          {
            begin: /\.\./,
            relevance: 0
          }
        ]
      }
    ]
  };
}

Notes

  • At a minimum the STATEMENTS, GLOBAL_COMMANDS and COMMON_COMMANDS can be pretty easily updated from /console/inspect data which I produce JSON in inspect.json downloadable from:
  • And colors can come from RouterOS LSP where I recently just mapped colors:
3 Likes

So I turned Gemini LLM to this problems, at least for the known keywords redacted below since the list 70k:

{
    "cmd": [
        "beep",
        "edit",
        "export",
        "get",
        "print",
        "set",
        "add",
        "hw-info",
        "while"
    ],
    "arg": [
        "as-value",
        "frequency",
        "length",
        "value-name",
        "datapath.interface-list",
        "datapath.local-forwarding",
        "datapath.openflow-switch",
        "request",
        "log-script-errors",
        "sanitize-names",
        "tab-width",
        "auto-restart-interval",
        "check-certificate",
        "cmd",
        "start-on-boot",
        "registry-url",
        "tmpdir",
        "username",
        "key",
        "user-profile"
    ],
    "path": [
        "file",
        "interface",
        "ip",
        "ipv6",
        "system",
        "tool",
        "user"
    ]
}

Here is the dialog that got me some JavaScript that download the inspect.json my tikoci/restraml project already generates for /console/inspect:

https://g.co/gemini/share/818acb8f52eb

The FULL output is here:
extract-types-from-inspect-json.js.txt (2.7 KB)
The source code from Gemini that produced it is here:
extracted_types.json.txt (74.6 KB)

I suspect those list of attributes could be used mapped into the COMMON_COMMANDS and OBJECTS to at least be more accurate.

Now the implementation benefit from using inspect.json and Highlight.js’s “Sub-modes”, etc:
https://highlightjs.readthedocs.io/en/latest/language-guide.html
which preserve the hierarchy of dir, path, arg.

And if “more right” allow viewers to spot an error in post script

1 Like

Hijacking my thread to test Graphviz with.

Graphviz “Patchwork” Test

I tried current Graphviz in “patchwork” engine (“squarified treemapping”) to show /ip/firewall/filter stats. But, yeah, I cannot get it to render text well, while tooltip works & text is SVG… the text gets scaled by SVG, somewhere, unknown.

I used example from docs, and rendered the graphviz using scripting to follow example.

https://graphviz.org/docs/layouts/patchwork/

It kinda works since you can hover over the items to see the “comment”, and if it a “dynamic” rule it’s shown in grey.

{
     :put "
[graphviz engine=patchwork]
graph {
    layout=patchwork
    node [style=filled]
"
    :foreach a in=[/ip/firewall/filter/print stats as-value] do={
       :put "\"$[:pick ($a->".id") 1 10]\"  [ area=$($a->"packets") label=\"$($a->"chain") $($a->"action")\" tooltip=\"$($a->"comment")\" fontsize=128 fillcolor=$[(>[:if ($a->"dynamic") do={:return "silver"} else={:return "gold"}])] ]"
    }     
    :put "
}
[/graphviz]" 
}

22 forward drop 21 input accept 20 forward passthrough 1 input accept F input accept 11 input accept 2 input drop 3 input accept 17 input accept 1C input accept 1D input accept 18 input accept 4 input accept 1F input accept 5 input drop 6 forward accept 7 forward accept 8 forward fasttrack-connection 9 forward accept A forward drop B forward drop

This is the Graphviz the routeros script generated (since markup used here gets hidden)…

graph {
    layout=patchwork
    node [style=filled]
"22"  [ area=0 label="forward drop" tooltip="back-to-home-vpn" fontsize=128 fillcolor=silver ]
"21"  [ area=13 label="input accept" tooltip="back-to-home-vpn" fontsize=128 fillcolor=silver ]
"20"  [ area=150510 label="forward passthrough" tooltip="special dummy rule to show fasttrack counters" fontsize=128 fillcolor=silver ]
"1"  [ area=9305970 label="input accept" tooltip="defconf: accept established,related,untracked" fontsize=128 fillcolor=gold ]
"F"  [ area=491559 label="input accept" tooltip="accept EoIP tunnel" fontsize=128 fillcolor=gold ]
"11"  [ area=3937 label="input accept" tooltip="accept SSTP tunnel on 8443" fontsize=128 fillcolor=gold ]
"2"  [ area=476 label="input drop" tooltip="defconf: drop invalid" fontsize=128 fillcolor=gold ]
"3"  [ area=29745 label="input accept" tooltip="defconf: accept ICMP" fontsize=128 fillcolor=gold ]
"17"  [ area=374 label="input accept" tooltip="allow btest (TCP)" fontsize=128 fillcolor=gold ]
"1C"  [ area=310 label="input accept" tooltip="allow https (TCP)" fontsize=128 fillcolor=gold ]
"1D"  [ area=1153 label="input accept" tooltip="allow http (TCP)" fontsize=128 fillcolor=gold ]
"18"  [ area=74 label="input accept" tooltip="allow btest (UDP)" fontsize=128 fillcolor=gold ]
"4"  [ area=300 label="input accept" tooltip="defconf: accept to local loopback (for CAPsMAN)" fontsize=128 fillcolor=gold ]
"1F"  [ area=375888 label="input accept" tooltip="drop all not coming from LAN (via address-list)" fontsize=128 fillcolor=gold ]
"5"  [ area=745097 label="input drop" tooltip="defconf: drop all not coming from LAN" fontsize=128 fillcolor=gold ]
"6"  [ area=0 label="forward accept" tooltip="defconf: accept in ipsec policy" fontsize=128 fillcolor=gold ]
"7"  [ area=0 label="forward accept" tooltip="defconf: accept out ipsec policy" fontsize=128 fillcolor=gold ]
"8"  [ area=156876 label="forward fasttrack-connection" tooltip="defconf: fasttrack" fontsize=128 fillcolor=gold ]
"9"  [ area=156876 label="forward accept" tooltip="defconf: accept established,related, untracked" fontsize=128 fillcolor=gold ]
"A"  [ area=349 label="forward drop" tooltip="defconf: drop invalid" fontsize=128 fillcolor=gold ]
"B"  [ area=0 label="forward drop" tooltip="defconf: drop all from WAN not DSTNATed" fontsize=128 fillcolor=gold ]
}

And, perhaps color=red or color=green for action=drop|accept & would better example & using gradients for “forward” vs “input/output” etc … - more food for thought.

FWIW, I think you’d “scale” – to ridiculous levels it gets scaled back for view. If you zoom in above, you’d see the text is rendered. So if fontsize= if calculated based on ($a->“packets”) or ($a->“bytes”), like some more complex version of fontsize=$bytes/1024 (where bytes is huge, thus need some check on min, etc. )

Trying LLMs on MikroTik Packet Flows

I wanted to see what LLMs could do with the Packet Flow diagrams in docs. The more complex one it did not do well. I tried a simpler one against the “Big 3”. I didn’t continue the conversation so a these “first prompt” results.

The all botched the actual flow… but did get various “template” for it.

Prompt

I’m looking to generate some .dot graphs based on an image. can you convert this to a graphviz dot format. perhaps and if you had thoughts on best graphviz layout engine for it, that be good to know too.

Claude

https://claude.ai/public/artifacts/1c80a79b-32a2-49e2-a6f6-a3597f050c1a

network_flow cluster_prerouting Chain 1: PREROUTING cluster_input Chain 2: INPUT cluster_forward Chain 3: FORWARD cluster_output Chain 4: OUTPUT cluster_postrouting Chain 5: POSTROUTING PREROUTING PREROUTING HOTSPOT_IN HOTSPOT-IN PREROUTING->HOTSPOT_IN INPUT INPUT MANGLE_INPUT MANGLE INPUT INPUT->MANGLE_INPUT FORWARD FORWARD BRIDGE_DECISION_1 BRIDGE DECISION FORWARD->BRIDGE_DECISION_1 OUTPUT OUTPUT BRIDGE_DECISION_2 BRIDGE DECISION OUTPUT->BRIDGE_DECISION_2 POSTROUTING POSTROUTING MANGLE_POSTROUTING MANGLE POSTROUTING POSTROUTING->MANGLE_POSTROUTING CONNECTION_TRACKING_1 CONNECTION TRACKING HOTSPOT_IN->CONNECTION_TRACKING_1 MANGLE_PREROUTING MANGLE PREROUTING CONNECTION_TRACKING_1->MANGLE_PREROUTING DST_NAT DST-NAT MANGLE_PREROUTING->DST_NAT FILTER_INPUT FILTER INPUT MANGLE_INPUT->FILTER_INPUT HTB_GLOBAL_QUEUE_TREE_1 HTB GLOBAL (QUEUE TREE) FILTER_INPUT->HTB_GLOBAL_QUEUE_TREE_1 SIMPLE_QUEUES_1 SIMPLE QUEUES HTB_GLOBAL_QUEUE_TREE_1->SIMPLE_QUEUES_1 TTL_MINUS_1 TTL=TTL-1 BRIDGE_DECISION_1->TTL_MINUS_1 MANGLE_FORWARD MANGLE FORWARD TTL_MINUS_1->MANGLE_FORWARD FILTER_FORWARD FILTER FORWARD MANGLE_FORWARD->FILTER_FORWARD ACCOUNTING ACCOUNTING FILTER_FORWARD->ACCOUNTING CONNECTION_TRACKING_2 CONNECTION TRACKING BRIDGE_DECISION_2->CONNECTION_TRACKING_2 MANGLE_OUTPUT MANGLE OUTPUT CONNECTION_TRACKING_2->MANGLE_OUTPUT FILTER_OUTPUT FILTER OUTPUT MANGLE_OUTPUT->FILTER_OUTPUT ROUTING_ADJUSTMENT ROUTING ADJUSTMENT FILTER_OUTPUT->ROUTING_ADJUSTMENT SRC_NAT SRC-NAT MANGLE_POSTROUTING->SRC_NAT HOTSPOT_OUT HOTSPOT-OUT SRC_NAT->HOTSPOT_OUT HTB_GLOBAL_QUEUE_TREE_2 HTB GLOBAL (QUEUE TREE) HOTSPOT_OUT->HTB_GLOBAL_QUEUE_TREE_2 SIMPLE_QUEUES_2 SIMPLE QUEUES HTB_GLOBAL_QUEUE_TREE_2->SIMPLE_QUEUES_2

Gemini

https://g.co/gemini/share/0bc906108583

G cluster_prerouting PREROUTING (1) cluster_input INPUT cluster_forward FORWARD (3) cluster_output OUTPUT cluster_postrouting POSTROUTING (4) prerouting_start PREROUTING hotspot_in_1A HOTSPOT-IN (1A) prerouting_start->hotspot_in_1A connection_tracking_1B CONNECTION TRACKING (1B) hotspot_in_1A->connection_tracking_1B mangle_prerouting_1C MANGLE PREROUTING (1C) connection_tracking_1B->mangle_prerouting_1C dst_nat_1D DST-NAT (1D) mangle_prerouting_1C->dst_nat_1D input_start INPUT mangle_input MANGLE INPUT input_start->mangle_input filter_input FILTER INPUT mangle_input->filter_input htb_global_input HTB GLOBAL (QUEUE TREE) filter_input->htb_global_input simple_queues_input SIMPLE QUEUES htb_global_input->simple_queues_input forward_start FORWARD bridge_decision_3A BRIDGE DECISION (3A) forward_start->bridge_decision_3A ttl_minus_1 TTL=TTL-1 bridge_decision_3A->ttl_minus_1 mangle_forward_3B MANGLE FORWARD (3B) ttl_minus_1->mangle_forward_3B filter_forward_3C FILTER FORWARD (3C) mangle_forward_3B->filter_forward_3C accounting_3D ACCOUNTING (3D) filter_forward_3C->accounting_3D output_start OUTPUT bridge_decision_output BRIDGE DECISION output_start->bridge_decision_output connection_tracking_output CONNECTION TRACKING bridge_decision_output->connection_tracking_output mangle_output MANGLE OUTPUT connection_tracking_output->mangle_output filter_output FILTER OUTPUT mangle_output->filter_output routing_adjustment ROUTING ADJUSTMENT filter_output->routing_adjustment postrouting_start POSTROUTING mangle_postrouting_4A MANGLE POSTROUTING (4A) postrouting_start->mangle_postrouting_4A src_nat_4B SRC-NAT (4B) mangle_postrouting_4A->src_nat_4B hotspot_out_4C HOTSPOT-OUT (4C) src_nat_4B->hotspot_out_4C htb_global_postrouting_4D HTB GLOBAL (QUEUE TREE) (4D) hotspot_out_4C->htb_global_postrouting_4D simple_queues_postrouting_4E SIMPLE QUEUES (4E) htb_global_postrouting_4D->simple_queues_postrouting_4E

ChatGPT

https://chatgpt.com/share/6862b90f-9558-8004-8192-cff8bc560a35

RouterOS_PacketFlow cluster_prerouting PREROUTING cluster_input INPUT cluster_forward FORWARD cluster_output OUTPUT cluster_postrouting POSTROUTING HOTSPOT_IN HOTSPOT-IN CONN_TRACK CONNECTION TRACKING HOTSPOT_IN->CONN_TRACK MANGLE_PREROUTING MANGLE PREROUTING CONN_TRACK->MANGLE_PREROUTING DST_NAT DST-NAT MANGLE_PREROUTING->DST_NAT MANGLE_INPUT MANGLE INPUT DST_NAT->MANGLE_INPUT FILTER_INPUT FILTER INPUT MANGLE_INPUT->FILTER_INPUT HTB_GLOBAL_INPUT HTB GLOBAL (QUEUE TREE) FILTER_INPUT->HTB_GLOBAL_INPUT SIMPLE_QUEUES_INPUT SIMPLE QUEUES HTB_GLOBAL_INPUT->SIMPLE_QUEUES_INPUT BRIDGE_DECISION_FWD BRIDGE DECISION SIMPLE_QUEUES_INPUT->BRIDGE_DECISION_FWD TTL_DEC TTL=TTL-1 BRIDGE_DECISION_FWD->TTL_DEC MANGLE_FORWARD MANGLE FORWARD TTL_DEC->MANGLE_FORWARD FILTER_FORWARD FILTER FORWARD MANGLE_FORWARD->FILTER_FORWARD ACCOUNTING ACCOUNTING FILTER_FORWARD->ACCOUNTING BRIDGE_DECISION_OUT BRIDGE DECISION ACCOUNTING->BRIDGE_DECISION_OUT CONN_TRACK_OUT CONNECTION TRACKING BRIDGE_DECISION_OUT->CONN_TRACK_OUT MANGLE_OUTPUT MANGLE OUTPUT CONN_TRACK_OUT->MANGLE_OUTPUT FILTER_OUTPUT FILTER OUTPUT MANGLE_OUTPUT->FILTER_OUTPUT ROUTING_ADJUSTMENT ROUTING ADJUSTMENT FILTER_OUTPUT->ROUTING_ADJUSTMENT MANGLE_POSTROUTING MANGLE POSTROUTING ROUTING_ADJUSTMENT->MANGLE_POSTROUTING SRC_NAT SRC-NAT MANGLE_POSTROUTING->SRC_NAT HOTSPOT_OUT HOTSPOT-OUT SRC_NAT->HOTSPOT_OUT HTB_GLOBAL_POST HTB GLOBAL (QUEUE TREE) HOTSPOT_OUT->HTB_GLOBAL_POST SIMPLE_QUEUES_POST SIMPLE QUEUES HTB_GLOBAL_POST->SIMPLE_QUEUES_POST

You are very lucky that nowadays colours are digital and cost nothing, if you were a painter in the Renaissance you would have been fired immediately for the wasted gold and silver on your first graph. :grinning_face:

1 Like

Subgraphs in Graphviz

Using the example from Graphviz’s docs for “Osage” engine – which looks to deal with “clusters” betters.

https://graphviz.org/docs/layouts/


graph {
		subgraph cluster_0 {
			label="composite cluster";
			subgraph cluster_1 {
			    label="the first cluster";
				C
				L
				U
				S
				T
				E
				R
			}
			subgraph cluster_2 {
			    label="the second\ncluster";
				a
				b
				c
				d
			}
			1
			2
		}
	3
	4
	5
}

Osage — [graphviz engine=osage]

https://graphviz.org/docs/layouts/osage/

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

DOT — [graphviz engine=dot]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

NOP[1] — [graphviz engine=nop]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

Neato — [graphviz engine=neato]

cluster_0 composite cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

FDP — [graphviz engine=fdp]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

SFDP — [graphviz engine=sfdp]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

Circo — [graphviz engine=circo]

C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

Twopi — [graphviz engine=twopi]

C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

NOP2 — [graphviz engine=nop2]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

Patchwork — [graphviz engine=patchwork]

cluster_0 composite cluster cluster_1 the first cluster cluster_2 the second cluster C C L L U U S S T T E E R R a a b b c c d d 1 1 2 2 3 3 4 4 5 5

Slight revised script to save the precious metals, “LawnGreen” and “Firebrick” now used based on drop/accept. But 16M colors using HEX, and color name follow X11 colors (although there is some colorscheme= option).

Revised [graphviz engine=patchwork] Example

{
:put "
[graphviz engine=patchwork]
graph {
	layout=patchwork
  node [style=filled]
"
:foreach a in=[/ip/firewall/filter/print stats as-value] do={
  :local fontsize (3*1024)
  :local color "CornflowerBlue"
  :if (($a->"action")~"(drop|tcp-reset)") do={:set color "Firebrick"}
  :if (($a->"action")~"(accept)") do={:set color "LawnGreen"}
  :local extra ""
  :if (($a->"chain")~"(forward)") do={:set extra "gradientangle=90"}
  :put "\"$[:pick ($a->".id") 1 10]\"  [ area=$($a->"packets") label=\"$($a->"chain") $($a->"action")\" tooltip=\"$($a->"comment")\" fontsize=$fontsize  fillcolor=$color $extra ]"
}     
:put "}
[/graphviz]" 
}

and now “wasting” variables & whitespace, and no fun (>[:if]) syntax

22 forward drop 21 input accept 20 forward passthrough 1 input accept F input accept 11 input accept 2 input drop 3 input accept 17 input accept 1C input accept 1D input accept 18 input accept 4 input accept 1F input accept 5 input drop 6 forward accept 7 forward accept 8 forward fasttrack-connection 9 forward accept A forward drop B forward drop

Removing “Input Accept” as test…

22 forward drop 21 input accept 20 forward passthrough F input accept 11 input accept 2 input drop 3 input accept 17 input accept 1C input accept 1D input accept 18 input accept 4 input accept 1F input accept 5 input drop 6 forward accept 7 forward accept 8 forward fasttrack-connection 9 forward accept A forward drop B forward drop

I actually hate picking colors. I turned Claude on www.mikrotik.com/logo page’s “approved” color ranges to find ones that overlap with X11 color names, to limit the process.

So if one wanted to stick to MikroTik’s color schemes, here are the X11 colors for that:

Color Name Hex Value Found in Range(s)
DarkCyan
#008B8B 8
DarkRed #8B0000 3
DarkSlateBlue #483D8B 6, 8
DarkSlateGray #2F4F4F 9, 10
DarkSlateGrey #2F4F4F 9, 10
Indigo #4B0082 8
Maroon #800000 1, 3
MidnightBlue #191970 2
Navy #000080 2
Purple #800080 1, 6
RoyalBlue #4169E1 4
SlateBlue #6A5ACD 4, 6
SteelBlue #4682B4 4
Teal #008080 8, 9
From,To
#C33366,#692878
#015EA4,#012D4E
#CF0F14,#5F0A0A
#87D3DB,#1F417A
#EE9B01,#EE4F01
#3660B9,#5F2965
#3BB5B6,#44DE95
#582D7C,#1FC8DB
#017C65,#2C3A43
#A3D16E,#155757
#0E0E10,#0E0E10
#707372,#707372
#C8C8C7,#C8C8C7

Much more modern, now It resembles vaguely Mondrian (though the acid green should be registered as improvised weapon).
Very good work with the X11/hex palette!

Not my first rodeo at trying graphviz. Graphviz makes even color selection, overwhelming, see:

https://graphviz.org/doc/info/colors.html#brewer

https://graphviz.org/docs/attrs/colorscheme/

…which is how one just decide to cut-and-paste from examples… and make a case for figuring out Mermaid plugin for forum.

Esoteric Markdown Tests

For testing “compatibility” with TikBook Markdown.

Both ~~~ and ``` are code fence blocks…

```routeros

/ip address add name=jdjsk
/ip/route/find gateway=1.1.1.1 comment~"WAN"

~~~routeros

/ip address add name=jdjsk
/ip/route/find gateway=1.1.1.1 comment~"WAN"

Comment are supported

Using <!-- HTML --> comments

Text contains one above which does not display

[//]: # (comment or metadata)

Text contains one above which does not display

Tilde being “the other” missing character in Italian keyboard, only It has a three digits Alt code (one more than backtick, 96), 126.
Thanks goodness on my laptop (without numpad) I have an UK keyboard.

Testing an updated version of my TikBook VSCode Notebook… it turns out VSCode uses the exact same Markdown engine as Discourse. And, apparently, same plugin RouterOS plugin for highlight.js & and it’s flaws. So even with my RouterOS LSP providing colors, apparently Markdown Preview does not use any VSCode colors, and uses highlight.js for Markdown preview.

Example of where highlight.js RouterOS syntax colorizer fails… $PIANO
Apparently a ’ in a comment seems to cause it to stop parsing.

And you can see pretty visual the RouterOS LSP (which uses exact same color scheme as CLI) and highlight.js’s RouterOS coloring:

# version 1.2

:global PIANO do={
    # required for recussion later
    :global PIANO
    # default note time is 200ms for 1/8 note (1x)
    :local nms 200ms
    # change note  length by arg using $PIANO ms=250ms
    :if ([:typeof $ms]="str") do={:set nms [:totime "0.$ms"]}
    :if ([:typeof $ms]="time") do={:set nms $ms}
    # or, use BPM via bpm= to control the length of 1/4 note
    :local lbpm (60000 / (([:tonsec [:totime $nms]] / 1000000) * 2))
    :if ([:typeof $bpm]~"(num|str)") do={
        :set lbpm [:tonum $bpm]
        :set nms [:totime "0.$(60000 / $bpm / 2)"]
    }
    # handle silent=yes (for output recording without playing)
    :local lsilent "no"
    :if ($silent="yes") do={ :set lsilent "yes" }
    # handle 'as-value' to return array, instead of output script
    :local asvalue 0
    :if ([:tostr $1]="as-value") do={:set asvalue 1}

    # array map of keypress to the Hz values for octaves 1 to 9
    #   note: k o l are +1 octave, so those are shifted by 1
    :local scalearr {"a"=("C",33,65,131,262,523,1047,2093,4186,8372) ;
    "w"=("C#",35,69,139,277,554,1109,2217,4435,8870) ;
    "s"=("D",37,73,147,294,587,1175,2349,4699,9397) ;
    "e"=("D#",39,78,156,311,622,1245,2489,4978,9956) ;
    "d"=("E",41,82,165,330,659,1319,2637,5274,10548) ;
    "f"=("F",44,87,175,349,698,1397,2794,5588,11175) ;
    "t"=("F#",46,92,185,370,740,1480,2960,5920,11840) ;
    "g"=("G",49,98,196,392,784,1568,3136,6272,12544) ;
    "y"=("G#",52,104,208,415,831,1661,3322,6645,13290) ;
    "h"=("A",55,110,220,440,880,1760,3520,7040,14080) ;
    "u"=("A#",58,117,233,466,932,1865,3729,7459,14917) ;
    "j"=("B",62,123,247,494,988,1976,3951,7902,15804) ;
    "k"=("+C",65,131,262,523,1047,2093,4186,8372,16744) ;
    "o"=("+C#",139,277,554,1109,2217,4435,8870,17739) ;
    "l"=("+D",73,147,294,587,1175,2349,4699,9397,18795) ;
    }
    # script needs to map numeric ASCII keycode to a string type with letter
    :local asciimap {"";"";"";"";"";"";"";"";"back";"";"tab";"";"";"enter";"return";"";"";"";"";"";"";"";"";"";"";"";"";"ESC";"";"";"";"";"space";"!";"\"";"";"\$";"%";"&";"";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"\?";"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_";"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"delete"}
    # current note size in ms - can be adjusted using 1-8 keys while playing
    :local lnms $nms
    # current octave, default is 4
    :local octave 4
    # current "eighth"
    :local neighth 1
    # store if "recording" and notes played
    :local record 1
    :local played [:toarray ""]
    # ...recording stopped when $record is set to 0, on with 1
    #    notes are pushed to a array "list"
    #    with each element in "outer" list being a list of two values:
    #    ($freq,$lnms) e.g. {(440,125),(440,125)}

    # helper function to format note as C3
    :local getnotename do={
        :local notename $2
        :if ([:len $2] != 0) do={
            :if ([:pick $notename 0 1]="+") do={
                # used higher octave keys like j i k
                :set notename "$[:pick $notename 1 8]$($1 + 1)"
            } else={
                :set notename "$notename$[:tostr $1]"
            }
        } else={:set notename ""}
        :return $notename
    }
    # helper function to print status line on update
    :local printstatus do={
        :local reconoff ""
        :local reccount ""
        # recording ON or OFF
        :if ([:tonum $3]!=0) do={:set reconoff "\1B[1;35mON "} else={:set reconoff "\1B[2;35mOFF"}
        # pretty display of record counter (length of played)
        :for lrec from=1 to=(4-[:len [:tostr [:len $4]]]) do={:set reccount "0$reccount"}
        :set reccount "$reccount$[:tostr [:len $4]]"
        :local notename $7
        # replace last status line, with new status line
        /terminal cuu
        :local notelenstr "$6/8"
        :if ("$6" = "4") do={:set notelenstr "1/2"}
        :if ("$6" = "2") do={:set notelenstr "1/4"}
        :if ("$6" = "6") do={:set notelenstr "3/4"}
        :if ("$6" = "8") do={:set notelenstr " 1 "}
        :put "\t\1B[1;34m$[:pick $2 7 16]s\1B[0m   \1B[2;31mOCTAVE\1B[0m \1B[1;31m$1\1B[0m  \1B[1;34m$notelenstr\1B[0m   \1B[1;35m$reccount\1B[0m \1B[2;35mRECORD\1B[0m $reconoff\1B[0m  \1B[1;7m$notename\1B[0m \1B[1;1m$5\1B[0m      "
    }

    # IF \$PIANO is called with a play=$myrecording, play that and exit
    :if ([:typeof $1]="array") do={
        # optional: store the saved recording, so it's returned again
        # :set played $play
        :set played $1
        :put ""
        :put " # SCRIPT TO PLAY RECORDING"
        :foreach rnote in=$played do={
            :if ([:typeof ($rnote->0)]="num" && [:typeof ($rnote->1)]~"(time|num)") do={
                :if (($rnote->0) > 19) do={
                    # play regular note
                    :if ($lsilent!="yes") do={
                        /beep freq=($rnote->0) length=($rnote->1)
                        /terminal/cuu
                    }
                    :put "     \1B[1;35m /beep\1B[0m  \1B[2;34mlength=\1B[0m\1B[1;34m$[:pick [:tostr ($rnote->1)] 7 16]\1B[0m \1B[2;31mfreq=\1B[0m\1B[1;31m$($rnote->0)\1B[0m \1B[2;31m; (\"\1B[0m\1B[1;7m$($rnote->2)\1B[0m\1B[2;31m\")\1B[0m \1B[2;35m; :delay $[:pick [:tostr ($rnote->1)] 7 16]\1B[0m     "
                } else={
                    # either "marker" - no delay, but comment
                    :if (($rnote->1) = 0) do={
                        :put "\t\t\t# MARK"
                    } else={
                        # or a "rest" - output the delay command
                        :put "\t\1B[1;35m     :delay $[:pick [:tostr ($rnote->1)] 7 16]\1B[0m     "
                    }
                }
                :if ($lsilent!="yes") do={
                    :delay ($rnote->1)
                }
            } else={
                :error "$[:tostr $rnote] contains invalid data"
            }
        }
        :return $played
    }

    # help screen
    :put "\1B[1;7m                    ROUTEROS PLAYER PIANO                    \1B[0m"
    :put "\1B[1;36mType a key to play a note...  1/8th note is $[:pick $nms 7 16]s."
    :put "\1B[2;36m\$PIANO takes ms= and bpm= to set the default note length"
    :put "\1B[1;36mTo play a longer note, use number key with #/8th of a note"
    :put "\1B[2;36m  1 == 1/8  2 == 1/4  4 = 1/2 ... 8 = whole note"
    :put "\1B[1;36mTo quit, hit 'q'"
    :put "\1B[1;36mUse 'x' for next higher octave, or 'z' to lower octave"
    :put "\1B[1;36mTo record, use ','|'.' to start|stop, <BS> to clear"
    :put "\1B[1;36mAny recording will be output as script after 'q'"
    :put "\1B[2;36m  to skip recording output use '\$PIANO as-value ms=120'"
    :put "\1B[2;36m  which will return an array of saved notes/rests/marks"
    :put "\1B[2;36m  e.g. ':global myrecording [\$PIANO as-value bpm=120]'"
    :put "\1B[1;36mTo later playback from var, use '\$PIANO \$myrecording'"
    :put "\1B[2;36m  with the array defined as {{freq;len};{freq;len},...}"
    :put "\1B[0m"
    :put "      \1B[2;34mLEN \1B[1;34m#\1B[2;34m/8\1B[0m \1B[1;34m 1\1B[0m \1B[1;34m 2\1B[0m \1B[1;34m 3\1B[0m \1B[1;34m 4\1B[0m \1B[1;34m 5\1B[0m \1B[1;34m 6\1B[0m \1B[1;34m 7\1B[0m \1B[1;34m 8\1B[0m   \1B[2;34mBPM \1B[1;34m$lbpm  \1B[1;35mCLEAR\1B[0m"
    :put "\t\1B[0m    `  1  2  3  4  5  6  7  8  9  0  -  = del"
    :put "\t      \1B[1;31mQUIT\1B[0m \1B[1;7mC#\1B[0m \1B[1;7mD#\1B[0m    \1B[1;7mF#\1B[0m \1B[1;7mG#\1B[0m \1B[1;7mA#\1B[0m    \1B[1;7mC#\1B[0m        \1B[1;35mMARK\1B[0m"
    :put "\t\1B[0m   tab  q  w  e  r  t  y  u  i  o  p  [  ]  \\"
    :put "\t         \1B[1;7mC\1B[0m  \1B[1;7mD\1B[0m  \1B[1;7mE\1B[0m  \1B[1;7mF\1B[0m  \1B[1;7mG\1B[0m  \1B[1;7mA\1B[0m  \1B[1;7mB\1B[0m  \1B[1;7mC\1B[0m  \1B[1;7mD\1B[0m        \1B[1mREST\1B[0m"
    :put "\t\1B[0m   caps  a  s  d  f  g  h  j  k  l  ;  '  ret"
    :put "\t\1B[1;31m          <  >               \1B[1;35mREC\1B[0m \1B[1;35mSTOP\1B[0m"
    :put "\t\1B[0m   shft   z  x  c  v  b  n  m  ,  .  /  shft"
    :put ""
    # first print of the status line
    $printstatus $octave $lnms $record $played 0 $neighth
    # start live player...
    # - loops per note time, plays notes per stored octave and keypress
    #   ends with q is pressed (ascii code 113)
    :local lastkey 65535
    :local lastfq 0
    :local notename ""
    :while ($lastkey != 113) do={
        # collect input
        :set lastkey [/terminal inkey]
        # 65535 means no keyboard input recieved before input timeout
        :if ($lastkey = 65535) do={:delay $nms} else={
            # if a number, use that as the multiplier for notes per second
            :if ($lastkey > 48 && $lastkey < 57) do={
                :set $neighth ($lastkey - 48)
                :set $lnms ($nms*$neighth)
                # update the display with new time per note
                $printstatus $octave $lnms $record $played $lastfq $neighth $notename
            }
            # convert the keypress ASCII code (num type) to a actual str type with a letter
            :local lastascii ($asciimap->$lastkey)
            :if ($lastkey = 60929) do={:set lastascii "left"}
            :if ($lastkey = 60930) do={:set lastascii "right"}
            :if ($lastkey = 60931) do={:set lastascii "up"}
            :if ($lastkey = 60932) do={:set lastascii "down"}
            # change octave via x or z
            :if ($lastascii ~ "x|z|left|right") do={
                :local newoctave
                :if ($lastascii~"z|left") do={ :set newoctave ($octave-1) } else={ :set newoctave ($octave +1) }
                :if ($newoctave > 0 && $newoctave < 10) do={
                    :set octave $newoctave
                }
            }
            # handle recording start/resume stop/start
            :if ($lastascii = ",") do={:set record 1}
            :if ($lastascii = ".") do={:set record 0}
            # handle clear recording
            :if ($lastascii = "back") do={:set played [:toarray ""]}
            # if enter key, that's a rest, which is stored as freq==0
            :if ($lastascii = "enter") do={
                # add the rest to stored recording
                :if ($record!=0) do={ :set $played ($played,{{0;$lnms}}) }
            }
            # handle mark recording
            :if ($lastascii = "\\") do={
                :if ($record!=0) do={ :set $played ($played,{{0;0}}) }
            }
            # fetch the actual Hz freq for the note, array is 0-based, so ->0 is 1st octave
            :local freq ($scalearr->$lastascii->($octave))
            # actually play the note
            :if ([:typeof $freq]="num") do={
                :if ($freq > 20) do={
                    :if ($silent!="yes") do={
                        :beep frequency=$freq length=$lnms
                    }
                    :set lastfq $freq
                    :set notename [$getnotename $octave ($scalearr->$lastascii->0)]
                    # if recording
                    :if ($record!=0) do={ :set $played ($played,{{$freq;$lnms;$notename}}) }
                }
                :delay $lnms
                /terminal cuu
            }
            $printstatus $octave $lnms $record $played $lastfq $neighth $notename
        }
    }
    :if ($asvalue=1) do={
        :return $played
    } else={
        $PIANO $played silent="yes"
        # in theory, should return nothing without "as-value"
        # but return anyway for easy-of-use
        :return $played
    }
}

:global myrecording [$PIANO bpm=150]

Side note, the __underlined__ text is screenshot is for non-local, non-global variables (like named parameters $silent vs local lsilent) — all that subtlety is lost by highlight.js