Difference between revisions of "Scripting: text editing"

From Freeplane - free mind mapping and knowledge management software
(Added more scripts from the original page)
m (Transpose the two characters around the cursor (for bad typists))
 
(33 intermediate revisions by 5 users not shown)
Line 5: Line 5:
 
Feel free to add your own scripts here. If you give script a name using wiki text like
 
Feel free to add your own scripts here. If you give script a name using wiki text like
  
<pre><groovy name="yourScriptName">
+
<pre><syntaxhighlight lang="Groovy" name="yourScriptName">
 
  your script
 
  your script
</groovy></pre>
+
</syntaxhighlight></pre>
  
 
an extra download button is created for it, and it can be downloaded directly from this page.
 
an extra download button is created for it, and it can be downloaded directly from this page.
  
For larger scripts there is a special [[Bazaar howto|Bazaar]] repository. Inspect it at [http://freeplane.bzr.sf.net/bzr/freeplane/contrib/groovy contrib/groovy] or get it via
+
For larger scripts there is a special [[Git howto|git]] repository [https://github.com/freeplane/addons].
 
 
  bzr branch bzr://freeplane.bzr.sourceforge.net/bzrroot/freeplane/contrib/groovy  [read only]
 
  bzr+ssh://USERNAME@freeplane.bzr.sourceforge.net/bzrroot/freeplane/contrib/groovy/ [developers only]
 
  
 
__TOC__
 
__TOC__
  
 
== Set the color for all children ==
 
 
This is an example of iteration over child nodes.
 
 
<groovy name="blueChildren">
 
node.children.each{ it.style.nodeTextColor = java.awt.Color.BLUE }
 
// @ExecutionModes({ON_SELECTED_NODE, ON_SELECTED_NODE_RECURSIVELY})
 
</groovy>
 
  
 
== Transpose the two characters around the cursor (for bad typists) ==
 
== Transpose the two characters around the cursor (for bad typists) ==
Line 32: Line 20:
 
Once this script is downloaded and placed in the script directory, it should be bound to a keystroke so that it can be called without moving from the keyboard. It is now also available as part of the Edit Goodies addon, as is the script below, to change case. That's much the easiest way to install it. This version should be considered a purely instructional example.
 
Once this script is downloaded and placed in the script directory, it should be bound to a keystroke so that it can be called without moving from the keyboard. It is now also available as part of the Edit Goodies addon, as is the script below, to change case. That's much the easiest way to install it. This version should be considered a purely instructional example.
  
<groovy name="transpose">
+
<syntaxhighlight lang="Groovy">
// @ExecutionModes({ON_SINGLE_NODE})
 
  
// first we import the Java libraries needed
 
// to access and manipulate the text of a node
 
import java.awt.KeyboardFocusManager
 
import javax.swing.JEditorPane
 
  
// Then wrap the action in a "try/catch" block so that it fails gracefully
+
  // @ExecutionModes({ON_SINGLE_NODE})
// though the record is written to a largely inaccessible log file, and should be displayed better
+
  // first we import the Java libraries needed
 
+
  // to access and manipulate the text of a node
try {  
+
  import java.awt.KeyboardFocusManager
 +
  import javax.swing.JEditorPane
 +
  // Then wrap the action in a "try/catch" block so that it fails gracefully
 +
  // though the record is written to a largely inaccessible log file, and should be displayed better
 +
  try {  
 
     def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner  
 
     def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner  
// these two lines check that the cursor is inside a node being actively edited
+
    // these two lines check that the cursor is inside a node being actively edited
 
     if (focusOwner instanceof JEditorPane) {  
 
     if (focusOwner instanceof JEditorPane) {  
 
         JEditorPane editor = focusOwner  
 
         JEditorPane editor = focusOwner  
// next, find the cursor position and save it in a variable
+
        // next, find the cursor position and save it in a variable
 
         cursor = editor.caretPosition - 1  
 
         cursor = editor.caretPosition - 1  
// Get a copy of the node text with all the html markup stripped out
+
        // Get a copy of the node text with all the html markup stripped out
 
         def oldstr = htmlUtils.htmlToPlain(editor.text)
 
         def oldstr = htmlUtils.htmlToPlain(editor.text)
// Make a new string with the letters around the cursor changed
+
        // Make a new string with the letters around the cursor changed
 
         def newstr = oldstr.substring(0, cursor-1) + oldstr[cursor] + oldstr[cursor-1] + oldstr.substring(cursor+1)  
 
         def newstr = oldstr.substring(0, cursor-1) + oldstr[cursor] + oldstr[cursor-1] + oldstr.substring(cursor+1)  
// And write it back into the node
+
        // And write it back into the node
 
         editor.text = newstr  
 
         editor.text = newstr  
// finally, put the cursor back where it was
+
        // finally, put the cursor back where it was
 
         editor.caretPosition = cursor + 1  
 
         editor.caretPosition = cursor + 1  
 
     }  
 
     }  
Line 62: Line 49:
 
         c.statusInfo = 'cannot transpose: inline editor not active'  
 
         c.statusInfo = 'cannot transpose: inline editor not active'  
 
     }  
 
     }  
}  
+
  }  
catch (Exception e) {  
+
  catch (Exception e) {  
 
     logger.severe('cannot transpose', e)  
 
     logger.severe('cannot transpose', e)  
}
+
  }
</groovy>
+
</syntaxhighlight>
  
 
Author: [[user:seatrout]] with polish and editing by [[user:boercher]]
 
Author: [[user:seatrout]] with polish and editing by [[user:boercher]]
Line 75: Line 62:
 
If used from here, it should be bound to a key
 
If used from here, it should be bound to a key
  
<groovy name="caseChanger">
+
  <syntaxhighlight lang="Groovy" name="caseChanger">
// @ExecutionModes({ON_SINGLE_NODE})  
+
  // @ExecutionModes({ON_SINGLE_NODE})  
// @CacheScriptContent(true)
+
  //import java.awt.Color
 
+
  import java.awt.KeyboardFocusManager  
 
+
  import javax.swing.JEditorPane  
//import java.awt.Color
+
  import java.text.BreakIterator
import java.awt.KeyboardFocusManager  
+
  // annotated a bit extra for instructional purposes
import javax.swing.JEditorPane  
+
  // starts with wrapping everything in a "try" block so that it crashes gracefully  
import java.text.BreakIterator
 
// annotated a bit extra for instructional purposes
 
// starts with wrapping everything in a "try" block so that it crashes gracefully  
 
  
try {  
+
    try {  
 
     // first find the node we are working in
 
     // first find the node we are working in
 
     def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner  
 
     def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner  
Line 93: Line 77:
 
         JEditorPane editor = focusOwner  
 
         JEditorPane editor = focusOwner  
 
         cursor = editor.caretPosition-1 //Here we find the cursor
 
         cursor = editor.caretPosition-1 //Here we find the cursor
oldWord=editor.getSelectedText()
+
        oldWord=editor.getSelectedText()
plaintext=htmlUtils.htmlToPlain(editor.text) // and here strip out the html for manipulating the text
+
        plaintext=htmlUtils.htmlToPlain(editor.text) // and here strip out the html for manipulating the text
isBigSelection=0
+
        isBigSelection=0
selectionStart=cursor //belt and braces to initialise this, but still
+
        selectionStart=cursor //belt and braces to initialise this, but still
// first, select the word the cursor is in, or merely hanging around
+
        // first, select the word the cursor is in, or merely hanging around
// if there is no text presently selected
+
        // if there is no text presently selected
if (oldWord==null){
+
        if (oldWord==null){
BreakIterator bi=BreakIterator.getWordInstance()
+
            BreakIterator bi=BreakIterator.getWordInstance()
bi.setText(plaintext)
+
            bi.setText(plaintext)
// this next bit should be simple, but is complicated by the need
+
            // this next bit should be simple, but is complicated by the need
// to do the right thing if the cursor is at either end of a word.
+
            // to do the right thing if the cursor is at either end of a word.
// the next block captures the case if it is at the beginning of a word
+
            // the next block captures the case if it is at the beginning of a word
if (bi.isBoundary(cursor) && (bi.isBoundary(cursor-1))){
+
            if (bi.isBoundary(cursor) && (bi.isBoundary(cursor-1))){
if (plaintext.substring(cursor-1,cursor) ==~/[A-z]/) {
+
                if (plaintext.substring(cursor-1,cursor) ==~/[A-z]/) {
//Problem! It could be a single-letter word, in which case
+
                    //Problem! It could be a single-letter word, in which case
oldWord=plaintext.substring(cursor-1,cursor)
+
                    oldWord=plaintext.substring(cursor-1,cursor)
selectionStart=cursor-2
+
                    selectionStart=cursor-2
}
+
                }
else {
+
                else {
oldWord=plaintext.substring(cursor,bi.next(2))
+
                    oldWord=plaintext.substring(cursor,bi.next(2))
}
+
                }
}
+
            }
else {
+
            else {
oldWord=plaintext.substring(bi.preceding(cursor),bi.next())
+
                oldWord=plaintext.substring(bi.preceding(cursor),bi.next())
selectionStart=bi.preceding(cursor)
+
                selectionStart=bi.preceding(cursor)
}
+
            }  
}
+
        }
// or there may be text selected, perhaps several words. in that case ...  
+
        // or there may be text selected, perhaps several words. in that case ...  
else {  
+
        else {  
selectionStart=editor.getSelectionStart()
+
            selectionStart=editor.getSelectionStart()
selectionEnd=editor.getSelectionEnd()
+
            selectionEnd=editor.getSelectionEnd()
oldWord=plaintext.substring(selectionStart-1,selectionEnd)
+
            oldWord=plaintext.substring(selectionStart-1,selectionEnd)
isBigSelection=1
+
            isBigSelection=1
//ui.informationMessage(ui.frame, oldWord, "The selection is")
+
            //ui.informationMessage(ui.frame, oldWord, "The selection is")
}
+
        }
// either way, now we have a selection
+
        // either way, now we have a selection
 
                 // and the next line does all the real work
 
                 // and the next line does all the real work
+
       
newWord=cycleCase(oldWord)
+
        newWord=cycleCase(oldWord)
  
// this next change changed to ensure that only one instance of a word is affected
+
        // this next change changed to ensure that only one instance of a word is affected
//(bug found in v 0.2)
+
        //(bug found in v 0.2)
  
editor.text=changeSelectedWordOnly(plaintext,selectionStart,oldWord,newWord)
+
        editor.text=changeSelectedWordOnly(plaintext,selectionStart,oldWord,newWord)
+
       
 
                 // now clean everything up and put the cursor back where it was  
 
                 // now clean everything up and put the cursor back where it was  
  
if(isBigSelection==0){
+
        if(isBigSelection==0){
editor.caretPosition = cursor+1
+
            editor.caretPosition = cursor+1
}
+
        }
else {
+
        else {
editor.setCaretPosition(selectionStart)
+
            editor.setCaretPosition(selectionStart)
editor.moveCaretPosition(selectionEnd)
+
            editor.moveCaretPosition(selectionEnd)
}
+
        }
+
       
}  
+
    }  
 
     else {  
 
     else {  
 
         c.statusInfo = 'cannot change case: inline editor not active'  
 
         c.statusInfo = 'cannot change case: inline editor not active'  
 
     }  
 
     }  
}  
+
  }  
catch (Exception e) {  
+
  catch (Exception e) {  
 
     logger.severe('cannot change case', e)  
 
     logger.severe('cannot change case', e)  
}
+
  }
  
// acb routine to ensure only the selection is changed
+
  // acb routine to ensure only the selection is changed
// could no doubt be more elegant but life is short
+
  // could no doubt be more elegant but life is short
  
def changeSelectedWordOnly(text,start,oldWord,newWord){
+
  def changeSelectedWordOnly(text,start,oldWord,newWord){
 
     firsthalf=text.substring(0,start)
 
     firsthalf=text.substring(0,start)
 
     secondhalf=text.substring(start)
 
     secondhalf=text.substring(start)
 
     firsthalf+secondhalf.replaceFirst(oldWord,newWord)
 
     firsthalf+secondhalf.replaceFirst(oldWord,newWord)
}
+
  }
  
// And the actual cycle case routine
+
  // And the actual cycle case routine
  
def cycleCase(oldWord){
+
  def cycleCase(oldWord){
if (oldWord.toUpperCase()==oldWord){
+
        if (oldWord.toUpperCase()==oldWord){
oldWord.toLowerCase()
+
            oldWord.toLowerCase()
}
+
        }
else if (oldWord.toLowerCase()==oldWord) {
+
        else if (oldWord.toLowerCase()==oldWord) {
oldWord[0].toUpperCase()+oldWord.substring(1).toLowerCase()
+
            oldWord[0].toUpperCase()+oldWord.substring(1).toLowerCase()
}
+
        }
else {
+
        else {
oldWord.toUpperCase()
+
            oldWord.toUpperCase()
}
+
        }
}
+
  }
</groovy>
+
  </syntaxhighlight>
  
 
Author: [[user:seatrout]] with polish and editing by [[user:boercher]]
 
Author: [[user:seatrout]] with polish and editing by [[user:boercher]]
Line 187: Line 171:
 
A toggle script for when you want to concentrate: will either centre the selected node, hide all others, and darken the background, or switch back to the normal bright, and possibly cluttered, view.
 
A toggle script for when you want to concentrate: will either centre the selected node, hide all others, and darken the background, or switch back to the normal bright, and possibly cluttered, view.
  
<groovy name="Cleanscreen">
+
<syntaxhighlight lang="Groovy" name="Cleanscreen">
// @ExecutionModes({ON_SINGLE_NODE})
+
  // @ExecutionModes({ON_SINGLE_NODE})
 
+
  import java.awt.Color
 
+
  import org.freeplane.core.resources.ResourceController
import java.awt.Color
+
  import org.freeplane.core.ui.ColorTracker
import org.freeplane.core.resources.ResourceController
+
  import org.freeplane.core.util.ColorUtils
import org.freeplane.core.ui.ColorTracker
+
  import org.freeplane.core.util.TextUtils
import org.freeplane.core.util.ColorUtils
+
  import org.freeplane.features.mode.Controller;
import org.freeplane.core.util.TextUtils
+
  import org.freeplane.features.styles.MapStyle
import org.freeplane.features.mode.Controller;
+
  import org.freeplane.features.styles.MapStyleModel
import org.freeplane.features.styles.MapStyle
+
  def setBackgroundColor(Color actionColor) {
import org.freeplane.features.styles.MapStyleModel
 
 
 
 
 
def setBackgroundColor(Color actionColor) {
 
 
     Controller controller = Controller.getCurrentController();
 
     Controller controller = Controller.getCurrentController();
 
     MapStyle mapStyle = (MapStyle) controller.getModeController().getExtension(MapStyle.class);
 
     MapStyle mapStyle = (MapStyle) controller.getModeController().getExtension(MapStyle.class);
     MapStyleModel model = (MapStyleModel) mapStyle.getMapHook();
+
     MapStyleModel model = (MapStyleModel) mapStyle.getMapHook(node.map.delegate);
 
     mapStyle.setBackgroundColor(model, actionColor);
 
     mapStyle.setBackgroundColor(model, actionColor);
}
+
  }
 
+
def setBackgroundColorCode(String rgbString) {
+
  def setBackgroundColorCode(String rgbString) {
 
     setBackgroundColor(ColorUtils.stringToColor(rgbString));
 
     setBackgroundColor(ColorUtils.stringToColor(rgbString));
}
+
  }
 
+
// note Groovy gotcha in the next line -- if "paleblue"  
+
  // note Groovy gotcha in the next line -- if "paleblue"  
// is defined as a colour explicitly, as in
+
  // is defined as a colour explicitly, as in
// "Color paleblue=new Color(218,230,239)"
+
  // "Color paleblue=new Color(218,230,239)"
// the script breaks, because variables defined in this way are not accessible inside blocks
+
  // the script breaks, because variables defined in this way are not accessible inside blocks
// they must instead be defined "naked" as below.
+
  // they must instead be defined "naked" as below.
// see http://groovy.codehaus.org/Scoping+and+the+Semantics+of+%22def%22
+
  // see http://groovy.codehaus.org/Scoping+and+the+Semantics+of+%22def%22
 
+
 
paleblue=new Color(218,230,239)
+
  paleblue=new Color(218,230,239)
 +
 
 +
 
 +
  if (this.node.style.edge.getColor()==paleblue){
 +
      clutterscreen()
 +
  }
 +
  else {
 +
      clearscreen()
 +
  }
 +
 
 +
 
 +
  def clearscreen(){
 +
    setBackgroundColor(paleblue)
 +
    myname=this.node.getId()
 +
    node.map.filter = { it.getId()==myname }
 +
    this.node.style.edge.setColor(paleblue)
 +
    c.centerOnNode(this.node)
 +
  }
 +
 
 +
  def clutterscreen(){
 +
      this.node.style.edge.setColor(Color.lightGray)
 +
      setBackgroundColor(Color.white)
 +
      node.map.undoFilter()
 +
  }
  
 
+
</syntaxhighlight>
if (this.node.style.edge.getColor()==paleblue){
 
clutterscreen()
 
}
 
else {
 
clearscreen()
 
}
 
 
 
 
 
def clearscreen(){
 
setBackgroundColor(paleblue)
 
myname=this.node.getId()
 
node.map.filter = { it.getId()==myname }
 
this.node.style.edge.setColor(paleblue)
 
c.centerOnNode(this.node)
 
}
 
 
 
def clutterscreen(){
 
this.node.style.edge.setColor(Color.lightGray)
 
setBackgroundColor(Color.white)
 
node.map.undoFilter()
 
}
 
 
 
 
 
 
 
</groovy>
 
  
 
Author: [[user:seatrout]] with the hard bits by [[user:boercher]]
 
Author: [[user:seatrout]] with the hard bits by [[user:boercher]]
 
==Insert Link to a node in another map==
 
 
This script allows to create a link to a node in another map. Follow this procedure to insert a link:
 
# Open the target map and select the target node
 
# Execute this script (by default ''Tools -> Scripts -> Insert Link To Node In Other Map -> ...on one selected node''. In the status line ''copied link'' will be printed
 
# Return to the node that should get the link (link source)
 
# Execute this script again. A dialog will appear asking if you want to insert the link - answer with ''Yes''
 
 
This functionality was suggested by various people, most recently by Andrés and Paul in [http://sourceforge.net/projects/freeplane/forums/forum/758437/topic/4683368 this discussion].
 
<groovy name="insertLinkToNodeInOtherMap">
 
// @ExecutionModes({ON_SINGLE_NODE})
 
import java.awt.Toolkit
 
import java.awt.datatransfer.Clipboard
 
import java.awt.datatransfer.DataFlavor
 
import java.awt.datatransfer.StringSelection
 
import java.awt.datatransfer.Transferable
 
 
import javax.swing.JOptionPane
 
 
import org.freeplane.core.resources.ResourceController
 
import org.freeplane.features.link.LinkController
 
 
URI getNodeUriFromClipboardIfAvailable() {
 
    Transferable selection = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this)
 
    if (selection == null || ! selection.isDataFlavorSupported(DataFlavor.stringFlavor))
 
        return null
 
    String selectionAsString = selection.getTransferData(DataFlavor.stringFlavor)
 
    try {
 
        def uri = new URI(selectionAsString)
 
        // only URIs with something after the '#' are proper links to nodes
 
        return uri.fragment && uri.path ? uri : null
 
    }
 
    catch (Exception e) {
 
        return null
 
    }
 
}
 
 
void copyLink() {
 
    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
 
    File file = node.map.file
 
    if (file == null) {
 
        JOptionPane.showMessageDialog(ui.frame,
 
            'The map must be saved to be usable as a link target', "Freeplane", JOptionPane.WARNING_MESSAGE);
 
        return
 
    }
 
    URI mapUri = node.map.file.absoluteFile.toURI();
 
    clipboard.setContents(new StringSelection('' + mapUri + '#' + node.id), null)
 
    c.statusInfo = 'copied link'
 
}
 
 
URI uri = getNodeUriFromClipboardIfAvailable()
 
if (uri != null && ! uri.path.contains(node.map.name)) {
 
    def title = 'Insert link to node in other map?'
 
    def result = ui.showConfirmDialog(c.selected?.delegate, title, title, JOptionPane.YES_NO_CANCEL_OPTION)
 
    if (result == JOptionPane.YES_OPTION) {
 
        boolean useRelativeUri = ResourceController.getResourceController().getProperty("links").equals("relative");
 
        if (useRelativeUri && node.map.file == null) {
 
            JOptionPane.showMessageDialog(ui.frame,
 
                'The map must be saved to be usable as a link source', "Freeplane", JOptionPane.WARNING_MESSAGE);
 
        }
 
        if (useRelativeUri)
 
            uri = new URI('' + LinkController.toRelativeURI(node.map.file, new File(uri.path)) + '#' + uri.fragment)
 
        node.link.text = uri
 
        c.statusInfo = 'link inserted'
 
    }
 
    else if (result == JOptionPane.NO_OPTION) {
 
        copyLink()
 
    }
 
}
 
else {
 
    copyLink()
 
}
 
</groovy>
 
Author: [[User:boercher]]
 
  
 
== Inserts an inline image into node text or details ==
 
== Inserts an inline image into node text or details ==
  
 
This script asks for an image file for inclusion as an inline graphics either into node details or node text. Plain texts will be converted to HTML. Thanks to Q [http://sourceforge.net/projects/freeplane/forums/forum/758437/topic/4617178 for asking for this feature].
 
This script asks for an image file for inclusion as an inline graphics either into node details or node text. Plain texts will be converted to HTML. Thanks to Q [http://sourceforge.net/projects/freeplane/forums/forum/758437/topic/4617178 for asking for this feature].
 +
 +
Note: This script is here just as an example for coding. If you just want to use it use the [https://www.freeplane.org/wiki/index.php/Add-ons#Insert_Inline_Image Insert Inline Image Add-On] instead.
  
 
Requires at least Freeplane 1.2.7
 
Requires at least Freeplane 1.2.7
  
<groovy name="insertInlineImage">
+
<syntaxhighlight lang="Groovy" name="insertInlineImage">
 
// @ExecutionModes({ON_SINGLE_NODE})
 
// @ExecutionModes({ON_SINGLE_NODE})
 
import groovy.swing.SwingBuilder
 
import groovy.swing.SwingBuilder
Line 413: Line 318:
 
         node.text = insertTag(node.text, imageTag)
 
         node.text = insertTag(node.text, imageTag)
 
}
 
}
</groovy>
+
</syntaxhighlight>
  
 
Author: [[User:boercher]]
 
Author: [[User:boercher]]
  
== Inline linked maps into one single map ==
+
== Simple plain text word count using scripts ==
 +
 
 +
This script will take the selected node(s), iterate down through subnodes, and count plain text. Notes, attributes, icons are not counted.
 +
 
 +
<syntaxhighlight lang="Groovy" name="countWords">
 +
  // @ExecutionModes({ON_SINGLE_NODE})
 +
  // Initial version: wordCount.groovy by User:cybaker
 +
  // Adapted for Freeplane by User:Boercher
 +
  // This script goes through all selected nodes and their subnodes and
 +
  // counts the number of plain text words, not including notes. Words
 +
  // are delimited by a any white space character.
 +
  // Message box for reporting added by User:seatrout
 +
  //
 +
  // Usage: Select node(s). Run script once.
  
This script replaces recursively all nodes with links to a map file (link text ending on '.mm') with the content of the respective map. Check logfile for files that could not be found/inlined. Thanks to Dennis [http://sourceforge.net/projects/freeplane/forums/forum/758437/topic/4619381 for asking for this feature].
+
  import java.text.NumberFormat;
  
Requires Freeplane after 1.2.8_02. With two minor adjustments also usable with older 1.2 versions (see script text).
+
  def nodes = new HashSet()
 +
  c.selecteds.each{ nodes.addAll(it.find{true}) }
  
<groovy name="inlineLinkedMaps">
+
  def result = nodes.sum(0){ it.plainTextContent.split("\\s+").length }
// @ExecutionModes({ON_SINGLE_NODE})
+
  ui.informationMessage(ui.frame, NumberFormat.getInstance().format(result)+ " Words in selected node and children", "Word Count")
def thisMap = node.map
+
 
def newMap = cloneMap(thisMap.root)
+
</syntaxhighlight>
inlineIncludes(newMap.root)
 
  
def cloneMap(rootNode) {
+
Author: [[User:cybaker]]
    def newMap = c.newMap()
 
    copyProperties(newMap.root, rootNode)
 
    rootNode.children.each{ newMap.root.appendBranch(it) }
 
    inlineIncludes(newMap.root)
 
    return newMap
 
}
 
  
// copies some of the various properties a node has from source to dest
+
== Delete line breaks ==
def copyProperties(dest, source) {
 
    dest.text = source.text
 
    // only after 1.2.8_02
 
    dest.attributes = source.attributes.map
 
    dest.link.text = source.link.text
 
    if (source.note != null)
 
        dest.note = source.note
 
    dest.details = source.detailsText
 
}
 
  
// inserts linked maps into the node containing the link
+
  Replace line breaks in the node text by space characters.
def inlineIncludes(rootNode) {
 
    rootNode.find{ it.link.text && it.link.text.endsWith(".mm") }.each { include ->
 
        def url = getURL(include)
 
        if (url) {
 
            def childMap = c.newMap(url)
 
            copyProperties(include, childMap.root)
 
            include.link.text = null
 
            childMap.root.children.each{ child ->
 
                include.appendBranch(child)
 
            }
 
            // leads to an -ignorable- error upto 1.2.8_02
 
            childMap.close(true, false)
 
        }
 
    }
 
}
 
  
// handles relative paths and more
+
  <syntaxhighlight lang="Groovy" name="deleteLineBreaks">
def getURL(node) {
+
 
    try {
+
  // @ExecutionModes({ON_SELECTED_NODE})
        def uri = node.link.uri
+
  node.text = node.text.replaceAll('\\s*\n\\s*', ' ')
        if (uri.scheme == null || uri.scheme == 'file') {
+
 
            def file = new File(uri.path)
+
</syntaxhighlight>
            if (!file.isAbsolute())
 
                file = new File(node.map.file.parent, uri.toString())
 
            if (!file.exists()) {
 
                logger.warn("invalid link " + file + " - cannot inline")
 
                return null
 
            }
 
            return file.toURL()
 
        }
 
        else {
 
            return uri.toURL()
 
        }
 
    } catch (Exception e) {
 
        logger.warn("invalid link " + node.link.text + " - cannot inline", e)
 
        return null
 
    }
 
}
 
</groovy>
 
  
 
Author: [[User:boercher]]
 
Author: [[User:boercher]]
  
== Simple plain text word count using scripts ==
+
== Note statistics ==
  
This script will take the selected node(s), iterate down through subnodes, and count plain text. Notes, attributes, icons are not counted.
+
This script prints note statistics to the status bar no matter if the note editor panel is opened or not. Although not strictly an editing script this one shows how to access the note editor panel.
  
<groovy name="countWords">
+
<syntaxhighlight lang="Groovy" name="noteStatistics">
// @ExecutionModes({ON_SINGLE_NODE})
+
// @ExecutionModes({ON_SINGLE_NODE})  
// Initial version: wordCount.groovy by User:cybaker
 
// Adapted for Freeplane by User:Boercher
 
// This script goes through all selected nodes and their subnodes and
 
// counts the number of plain text words, not including notes. Words
 
// are delimited by a any white space character.
 
//
 
// Usage: Select node(s). Run script once.
 
  
import java.text.NumberFormat;
+
import org.freeplane.features.note.NoteController
  
def nodes = new HashSet()
+
def noteTextFromViewer = NoteController.controller.noteViewerComponent?.documentText
c.selecteds.each{ nodes.addAll(it.find{true}) }
+
def noteText = noteTextFromViewer ? htmlUtils.htmlToPlain(noteTextFromViewer) : node.note?.to?.plain
 +
if (noteText) {
 +
def wordCount = noteText.split('\\W+').length
 +
c.statusInfo = "note: ${wordCount} words, ${noteText.length()} characters"
 +
}
 +
else {
 +
c.statusInfo = "no note"
 +
}
 +
</syntaxhighlight>
  
def result = nodes.sum(0){ it.plainTextContent.split("\\s+").length }
+
Author: [[User:boercher]]
c.statusInfo = NumberFormat.getInstance().format(result)
 
</groovy>
 
 
 
Author: [[User:cybaker]]
 

Latest revision as of 21:07, 27 December 2018

These are scripts which deal mostly with editing the text of nodes.

Some of them are also available in the editgoodies addon.

Feel free to add your own scripts here. If you give script a name using wiki text like

<syntaxhighlight lang="Groovy" name="yourScriptName">
 your script
</syntaxhighlight>

an extra download button is created for it, and it can be downloaded directly from this page.

For larger scripts there is a special git repository [1].


Transpose the two characters around the cursor (for bad typists)

Once this script is downloaded and placed in the script directory, it should be bound to a keystroke so that it can be called without moving from the keyboard. It is now also available as part of the Edit Goodies addon, as is the script below, to change case. That's much the easiest way to install it. This version should be considered a purely instructional example.

  // @ExecutionModes({ON_SINGLE_NODE}) 
  // first we import the Java libraries needed 
  // to access and manipulate the text of a node
  import java.awt.KeyboardFocusManager 
  import javax.swing.JEditorPane 
  // Then wrap the action in a "try/catch" block so that it fails gracefully
  // though the record is written to a largely inaccessible log file, and should be displayed better
  try { 
     def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner 
     // these two lines check that the cursor is inside a node being actively edited
     if (focusOwner instanceof JEditorPane) { 
         JEditorPane editor = focusOwner 
         // next, find the cursor position and save it in a variable
         cursor = editor.caretPosition - 1 
         // Get a copy of the node text with all the html markup stripped out
         def oldstr = htmlUtils.htmlToPlain(editor.text)
        // Make a new string with the letters around the cursor changed
         def newstr = oldstr.substring(0, cursor-1) + oldstr[cursor] + oldstr[cursor-1] + oldstr.substring(cursor+1) 
         // And write it back into the node
         editor.text = newstr 
         // finally, put the cursor back where it was
         editor.caretPosition = cursor + 1 
     } 
     else { 
         c.statusInfo = 'cannot transpose: inline editor not active' 
     } 
  } 
  catch (Exception e) { 
    logger.severe('cannot transpose', e) 
  }

Author: user:seatrout with polish and editing by user:boercher

Change case of words, and sort bAD CApitalisation

This script, like the one above, is part of the edit goodies addon, available from the support site. If used from here, it should be bound to a key

  // @ExecutionModes({ON_SINGLE_NODE}) 
  //import java.awt.Color
  import java.awt.KeyboardFocusManager 
  import javax.swing.JEditorPane 
  import java.text.BreakIterator
  // annotated a bit extra for instructional purposes
  // starts with wrapping everything in a "try" block so that it crashes gracefully 

    try { 
    // first find the node we are working in
    def focusOwner = KeyboardFocusManager.currentKeyboardFocusManager.focusOwner 
    if (focusOwner instanceof JEditorPane) { 
        JEditorPane editor = focusOwner 
        cursor = editor.caretPosition-1 //Here we find the cursor
        oldWord=editor.getSelectedText()
        plaintext=htmlUtils.htmlToPlain(editor.text) // and here strip out the html for manipulating the text
        isBigSelection=0
        selectionStart=cursor //belt and braces to initialise this, but still
        // first, select the word the cursor is in, or merely hanging around
        // if there is no text presently selected
        if (oldWord==null){
            BreakIterator bi=BreakIterator.getWordInstance()
            bi.setText(plaintext)
            // this next bit should be simple, but is complicated by the need
            // to do the right thing if the cursor is at either end of a word.
            // the next block captures the case if it is at the beginning of a word
            if (bi.isBoundary(cursor) && (bi.isBoundary(cursor-1))){
                if (plaintext.substring(cursor-1,cursor) ==~/[A-z]/) {
                    //Problem! It could be a single-letter word, in which case
                    oldWord=plaintext.substring(cursor-1,cursor)
                    selectionStart=cursor-2
                }
                else {
                    oldWord=plaintext.substring(cursor,bi.next(2))
                }
            }
            else {
                oldWord=plaintext.substring(bi.preceding(cursor),bi.next())
                selectionStart=bi.preceding(cursor)
            }    
        }
        // or there may be text selected, perhaps several words. in that case ... 
        else { 
            selectionStart=editor.getSelectionStart()
            selectionEnd=editor.getSelectionEnd()
            oldWord=plaintext.substring(selectionStart-1,selectionEnd)
            isBigSelection=1
            //ui.informationMessage(ui.frame, oldWord, "The selection is")
        }
        // either way, now we have a selection
                // and the next line does all the real work
        
        newWord=cycleCase(oldWord)

        // this next change changed to ensure that only one instance of a word is affected
        //(bug found in v 0.2)

        editor.text=changeSelectedWordOnly(plaintext,selectionStart,oldWord,newWord)
        
                // now clean everything up and put the cursor back where it was 

        if(isBigSelection==0){
            editor.caretPosition = cursor+1
        }
        else {
            editor.setCaretPosition(selectionStart)
            editor.moveCaretPosition(selectionEnd)
        }
        
    } 
    else { 
        c.statusInfo = 'cannot change case: inline editor not active' 
    } 
  } 
   catch (Exception e) { 
    logger.severe('cannot change case', e) 
  }

  // acb routine to ensure only the selection is changed
  // could no doubt be more elegant but life is short

  def changeSelectedWordOnly(text,start,oldWord,newWord){
    firsthalf=text.substring(0,start)
    secondhalf=text.substring(start)
    firsthalf+secondhalf.replaceFirst(oldWord,newWord)
  }

  // And the actual cycle case routine

  def cycleCase(oldWord){
        if (oldWord.toUpperCase()==oldWord){
            oldWord.toLowerCase()
        }
        else if (oldWord.toLowerCase()==oldWord) {
            oldWord[0].toUpperCase()+oldWord.substring(1).toLowerCase()
        }
        else {
            oldWord.toUpperCase()
        }
  }

Author: user:seatrout with polish and editing by user:boercher

Clean screen editor

A toggle script for when you want to concentrate: will either centre the selected node, hide all others, and darken the background, or switch back to the normal bright, and possibly cluttered, view.

  // @ExecutionModes({ON_SINGLE_NODE})
  import java.awt.Color
  import org.freeplane.core.resources.ResourceController
  import org.freeplane.core.ui.ColorTracker
  import org.freeplane.core.util.ColorUtils
  import org.freeplane.core.util.TextUtils
  import org.freeplane.features.mode.Controller;
  import org.freeplane.features.styles.MapStyle
  import org.freeplane.features.styles.MapStyleModel
  def setBackgroundColor(Color actionColor) {
    Controller controller = Controller.getCurrentController();
    MapStyle mapStyle = (MapStyle) controller.getModeController().getExtension(MapStyle.class);
    MapStyleModel model = (MapStyleModel) mapStyle.getMapHook(node.map.delegate);
    mapStyle.setBackgroundColor(model, actionColor);
  }
 
  def setBackgroundColorCode(String rgbString) {
    setBackgroundColor(ColorUtils.stringToColor(rgbString));
  }
 
  // note Groovy gotcha in the next line -- if "paleblue" 
  // is defined as a colour explicitly, as in
  // "Color paleblue=new Color(218,230,239)"
  // the script breaks, because variables defined in this way are not accessible inside blocks
  // they must instead be defined "naked" as below.
  // see http://groovy.codehaus.org/Scoping+and+the+Semantics+of+%22def%22
  
  paleblue=new Color(218,230,239)
  
  
  if (this.node.style.edge.getColor()==paleblue){
      clutterscreen()
  }
  else {
      clearscreen()
  }
  
  
  def clearscreen(){
    setBackgroundColor(paleblue)
    myname=this.node.getId()
    node.map.filter = { it.getId()==myname }
    this.node.style.edge.setColor(paleblue)
    c.centerOnNode(this.node)
  }
  
  def clutterscreen(){
      this.node.style.edge.setColor(Color.lightGray)
      setBackgroundColor(Color.white)
      node.map.undoFilter()
  }

Author: user:seatrout with the hard bits by user:boercher

Inserts an inline image into node text or details

This script asks for an image file for inclusion as an inline graphics either into node details or node text. Plain texts will be converted to HTML. Thanks to Q for asking for this feature.

Note: This script is here just as an example for coding. If you just want to use it use the Insert Inline Image Add-On instead.

Requires at least Freeplane 1.2.7

// @ExecutionModes({ON_SINGLE_NODE})
import groovy.swing.SwingBuilder
import java.awt.FlowLayout as FL
import javax.swing.BoxLayout as BXL
import javax.swing.ImageIcon
import javax.swing.JFileChooser
import javax.swing.JTextField
import org.freeplane.core.resources.ResourceController

def ImageIcon getIcon(String path) {
    new ImageIcon(ResourceController.getResourceController().getResource(path))
}

def builder = new SwingBuilder()
def dial = builder.dialog(title:'Insert Image', id:'insertImage', modal:true,
               locationRelativeTo:ui.frame, owner:ui.frame, pack:true, show:true) {
    panel() {
        JTextField urlField
        boxLayout(axis:BXL.Y_AXIS)
        panel(alignmentX:0f) {
            flowLayout(alignment:FL.LEFT)
            label('URL')
            urlField = textField(id:'url', columns:30)
            button(action:action(closure:{
                def chooser = fileChooser(fileSelectionMode:JFileChooser.FILES_ONLY)
                if (chooser.showOpenDialog() == JFileChooser.APPROVE_OPTION)
                    urlField.text = chooser.selectedFile.toURL()
                }), icon:getIcon("/images/fileopen.png"))
        }
        panel(alignmentX:0f) {
            flowLayout(alignment:FL.LEFT)
            label('Width:')
            textField(id:'width', columns:3)
            glue()
            label('Height:')
            textField(id:'height', columns:3)
        }
        panel(alignmentX:0f) {
            flowLayout(alignment:FL.LEFT)
            label('Target:')
            buttonGroup().with { group ->  
                radioButton(id:'text', text:'Node Text', selected:true, buttonGroup:group)  
                radioButton(id:'details', text:'Node Details', buttonGroup:group)  
            }  
        }
        panel(alignmentX:0f) {
            flowLayout(alignment:FL.RIGHT)
            button(action:action(name:'OK', defaultButton:true, mnemonic:'O',
                                 enabled:bind(source:urlField, sourceProperty:'text',
                                              converter:{ it ? true : false }),
                                 closure:{variables.ok = true; dispose()}))
            button(action:action(name:'Cancel', mnemonic:'C', closure:{dispose()}))
        }
    }
}

def String insertTag(String text, String htmlTag) {
    if (text == null)
        text = ""
    if ( ! text.startsWith("<html>"))
        text = "<html><head/><body>${text}</body></html>"
    return text.replace("</body>", htmlTag + "</body>")
}

def String imageTag(url, width, height) {
    def attribs = [ "src='${url}'" ]
    if (width)
        attribs << "width='${width}'"
    if (height)
        attribs << "height='${height}'"
    "<img ${attribs.join(' ')} />"
}

def vars = builder.variables
if (vars.ok) {
    def imageTag = imageTag(vars.url.text, vars.width.text, vars.height.text)
    if (vars.details.selected)
        node.details = insertTag(node.detailsText, imageTag)
    else
        node.text = insertTag(node.text, imageTag)
}

Author: User:boercher

Simple plain text word count using scripts

This script will take the selected node(s), iterate down through subnodes, and count plain text. Notes, attributes, icons are not counted.

  // @ExecutionModes({ON_SINGLE_NODE})
  // Initial version: wordCount.groovy by User:cybaker
  // Adapted for Freeplane by User:Boercher
  // This script goes through all selected nodes and their subnodes and
  // counts the number of plain text words, not including notes. Words
  // are delimited by a any white space character.
  // Message box for reporting added by User:seatrout
  //
  // Usage: Select node(s). Run script once.

  import java.text.NumberFormat;

  def nodes = new HashSet()
  c.selecteds.each{ nodes.addAll(it.find{true}) }

  def result = nodes.sum(0){ it.plainTextContent.split("\\s+").length }
  ui.informationMessage(ui.frame, NumberFormat.getInstance().format(result)+ " Words in selected node and children", "Word Count")

Author: User:cybaker

Delete line breaks

 Replace line breaks in the node text by space characters.
  
  // @ExecutionModes({ON_SELECTED_NODE})
  node.text = node.text.replaceAll('\\s*\n\\s*', ' ')

Author: User:boercher

Note statistics

This script prints note statistics to the status bar no matter if the note editor panel is opened or not. Although not strictly an editing script this one shows how to access the note editor panel.

// @ExecutionModes({ON_SINGLE_NODE}) 

import org.freeplane.features.note.NoteController

def noteTextFromViewer = NoteController.controller.noteViewerComponent?.documentText
def noteText = noteTextFromViewer ? htmlUtils.htmlToPlain(noteTextFromViewer) : node.note?.to?.plain
if (noteText) {
	def wordCount = noteText.split('\\W+').length
	c.statusInfo = "note: ${wordCount} words, ${noteText.length()} characters"
}
else {
	c.statusInfo = "no note"
}

Author: User:boercher