XQuery for converting to or from .bike files

XQuery gives quite a lot of power and convenience if you want to read or write .bike files.
There are various options for running XQuery on macOS.

XQuery 1.0

is available in JavaScript for Automation though a method on

NSXMLDocument

.objectsForXQueryError(xquerySting, errorObject)

but to define the recursive functions that simplify working over outlines,
you are much better off with XQuery 3.0

XQuery 3.0

  1. Saxon Professional Edition is highly compliant and well-maintained, doesn’t depend on Java, may well be available at your university, but costs £60 p.a. for personal individual use.

  2. Basex is free and well configured for casual users – probably the easiest to use, either in a GUI or from the command line.

  3. Exist-db is open source and seems well worth looking at, but I haven’t personally experimented with it, then

  4. there are some commercial offerings which might be available in some workplaces. Not available at my desk, however.

  • Marklogic,
  • Altova …

An example using Basex

Installation

One way of installing basex is via brew

brew install basex

Once it’s in place, you can launch the GUI (useful for experimentation) from the command line with:

basexgui

For more regular use, it may make sense to go straight to the command line.

For example, if you have declared file-name as a name for any externally supplied file, then you can bind that name to a particular file with a command line like:

basex -b file-name="mindmap.xml" -o "MyNewOutline.bike" convert-mindmap.xq

Where convert-mindmap.xq, (below) is, in this case, deriving a .bike version of a SimpleMind mind-map, preserving inline formats like strikethough, bold, etc, and mapping cross-connection arrows (between mindmap nodes) as hyperlinks between Bike rows.

(SimpleMind exports OPML, but OPML drops the inline formats and the arrows between otherwise unrelated nodes)

The source (which I’ve tested only on Basex and Saxon PE, after unzipping a SimpleMind document, and extracting its document/mindmap.xml) might look something like:

Expand disclosure triangle to view XQuery source
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization";

declare option output:method "xml";
declare option output:omit-xml-declaration "no";

declare variable $file-name external;

(: --- LINE BREAKS WITHIN SIMPLEMIND NODE TEXTS --- :)
declare function local:parse-line-breaks($text as xs:string) as node()* {
  let $parts := tokenize($text, '\\N')
  for $part at $pos in $parts
  return (
    if ($pos > 1) then text { "
" } else (),
    if ($part != "") then text { $part } else ()
  )
};

(: --- INLINE FORMATS --- :)
declare function local:parse-rtf($text as xs:string) as node()* {
  let $regex := '\\\\([*/~_^`])(.*?)\\\\\1'
  let $analyzed := analyze-string($text, $regex, 's')
  
  for $part in $analyzed/*
  return
    if ($part/local-name() = 'non-match') then
      local:parse-line-breaks(string($part))
      
    else if ($part/local-name() = 'match') then
      let $marker := string($part/*:group[@nr="1"])
      let $content := string($part/*:group[@nr="2"])
      let $inner-nodes := local:parse-rtf($content)
      
      return
        if ($marker = '*') then <strong>{ $inner-nodes }</strong>
        else if ($marker = '/') then <em>{ $inner-nodes }</em>
        else if ($marker = '~') then <s>{ $inner-nodes }</s>
        else if ($marker = '^') then <sup>{ $inner-nodes }</sup>
        else if ($marker = '`') then <sub>{ $inner-nodes }</sub>
        (: FIX: Bike ignores underlines, so we just pass the text through natively :)
        else if ($marker = '_') then $inner-nodes 
        else $inner-nodes
        
    else ()
};

(: --- RECURSION DOWN MINDMAP BRANCHES --- :)
declare function local:build-node(
  $all-topics as element(topic)*, 
  $relations as element(relation)*, 
  $current-topic as element(topic)
) as element(li) {
  
  let $id := ($current-topic/@guid, 'sm-node-' || $current-topic/@id)[1]
  let $children := $all-topics[@parent = $current-topic/@id]
  let $outgoing-relation := $relations[@source = $current-topic/@id][1]
  
  return
    <li id="{$id}">
      <p>
        {
          let $parsed-text := 
            if ($current-topic/@textfmt = 'rtf1') then
              local:parse-rtf(data($current-topic/@text))
            else
              local:parse-line-breaks(data($current-topic/@text))
          
          return
            if ($outgoing-relation) then
              let $target-topic := $all-topics[@id = $outgoing-relation/@target]
              let $target-id := ($target-topic/@guid, 'sm-node-' || $target-topic/@id)[1]
              return <a href="#{ $target-id }">{ $parsed-text }</a>
            else 
              $parsed-text
        }
      </p>
      {
        if (exists($children)) then
          <ul>
            {
              for $child in $children
              return local:build-node($all-topics, $relations, $child)
            }
          </ul>
        else ()
      }
    </li>
};

(: --- MAIN --- :)
let $doc := doc($file-name)
let $topics := $doc//topic
let $relations := $doc//relation
let $root-topic := $topics[@parent = "-1"]

return
<html>
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <ul id="root-list">
      { local:build-node($topics, $relations, $root-topic) }
    </ul>
  </body>
</html>

In the Basex GUI, you can specify a value for $file-name via the External Variables dialog ⇧⌘E

1 Like