Antes de começar

O que você vai aprender

  • Ter à mão scripts prontos para as operações repetitivas mais comuns
  • Entender o padrão que cada um usa, para adaptá-los ao seu caso
  • Organizar os seus scripts como biblioteca em vez de arquivos soltos
  • Instalar um script como item de menu com atalho de teclado

Biblioteca de Scripts Úteis🔗

Um script vale a pena quando a operação é repetitiva, mecânica e propensa a erro humano. Os que estão aqui cobrem esse perfil: coisas que dá para fazer à mão em vinte minutos e que o script resolve em um segundo, sempre do mesmo jeito.

Todos assumem que há um nó selecionado e operam sobre o ramo dele. Todos devem ser testados numa cópia antes de rodar num mapa que importa — o desfazer de operações em massa não é confiável.

Estes scripts precisam da permissão de executar scripts sem assinatura em Preferências → Scripts. O que grava arquivo precisa também de escrever arquivos. Conceda o mínimo de que precisa e revise essas permissões depois.

1 — Numerar filhos🔗

Prefixa cada filho direto com um número sequencial. Útil para transformar um brainstorm em uma sequência de passos.

// NumerarFilhos.groovy
node.children.eachWithIndex { filho, i ->
    def limpo = filho.text.replaceFirst(/^\d+\.\s*/, '')
    filho.text = "${i + 1}. ${limpo}"
}

O replaceFirst é o que torna o script idempotente: rodá-lo duas vezes não produz 1. 1. Item. Vale a pena escrever assim todo script que altera texto — mais cedo ou mais tarde alguém roda de novo por engano.

2 — Registrar profundidade em atributo🔗

Escreve o nível de cada nó do ramo num atributo. Serve para filtrar por profundidade, coisa que a interface não oferece diretamente.

// RegistrarNivel.groovy
def raiz = node
node.branch.each { n ->
    def d = 0
    def p = n
    while (p != raiz) { p = p.parent; d++ }
    n['nivel'] = d as String
}

Depois disso, um filtro por nivel = 3 mostra exatamente um estrato do mapa.

3 — Achar nós órfãos de conteúdo🔗

Lista os nós que não têm texto útil — sobras de edição que se acumulam em mapas velhos.

// AcharVazios.groovy
def vazios = node.branch.findAll {
    !it.plainText?.trim() && it.children.isEmpty()
}
if (vazios) {
    vazios.each { it.style.name = 'Atenção' }
    ui.informationMessage("${vazios.size()} nó(s) vazio(s) marcados com o estilo Atenção.")
} else {
    ui.informationMessage('Nenhum nó vazio encontrado.')
}

Note o padrão: o script marca em vez de apagar. Alterar o mapa é fácil; desfazer não. Marcar e deixar a decisão com você é quase sempre o desenho certo para um script de faxina.

4 — Copiar ícone para os descendentes🔗

Propaga o ícone do nó selecionado para todo o ramo. Útil ao classificar um bloco inteiro de uma vez.

// PropagarIcones.groovy
def icones = node.icons.icons
node.branch.each { n ->
    if (n == node) return
    icones.each { ic ->
        if (!n.icons.icons.contains(ic)) n.icons.add(ic)
    }
}

A checagem de contains mantém a idempotência: rodar de novo não empilha o mesmo ícone várias vezes.

5 — Extrair o ramo como Markdown🔗

Gera um outline em Markdown do ramo selecionado e o coloca na área de transferência — o caminho mais rápido para levar estrutura do mapa para um editor de texto.

// RamoParaMarkdown.groovy
def linhas = []
def percorrer
percorrer = { n, nivel ->
    linhas << ('  ' * nivel) + '- ' + n.plainText.trim()
    n.children.each { percorrer(it, nivel + 1) }
}
percorrer(node, 0)

def texto = linhas.join('\n')
java.awt.Toolkit.defaultToolkit.systemClipboard.setContents(
    new java.awt.datatransfer.StringSelection(texto), null)
ui.informationMessage("${linhas.size()} linhas copiadas.")

A recursão com uma closure atribuída antes de ser usada (def percorrer; percorrer = {...}) é o idioma Groovy para recursão anônima — declarar e atribuir na mesma linha não funciona, porque a variável ainda não existe dentro do próprio corpo.

6 — Relatório de atributos ausentes🔗

Mostra quantos nós do ramo não têm cada chave preenchida. É o diagnóstico que vale rodar antes de confiar em qualquer filtro.

// AtributosAusentes.groovy
def chaves = ['responsável', 'prazo', 'estado']
def total = node.branch.size()
def relato = chaves.collect { chave ->
    def faltam = node.branch.count { !it[chave] }
    "${chave}: ${faltam} de ${total} sem valor"
}.join('\n')
ui.informationMessage(relato)

Se o número de ausentes for alto, o filtro que você acha que funciona está devolvendo menos do que deveria — e em silêncio.

Organizando como biblioteca🔗

Scripts soltos numa pasta são difíceis de manter. O caminho melhor é extrair o que se repete para um arquivo próprio e importá-lo:

~/.config/freeplane/<versão>/
├── scripts/
│   ├── NumerarFilhos.groovy
│   ├── RegistrarNivel.groovy
│   └── ...
└── lib/
    └── Utilidades.groovy

Em lib/Utilidades.groovy:

class Utilidades {
    static int profundidade(no, raiz) {
        int d = 0
        def p = no
        while (p != raiz) { p = p.parent; d++ }
        return d
    }

    static String outline(no, int nivel = 0) {
        def s = ('  ' * nivel) + '- ' + no.plainText.trim() + '\n'
        no.children.each { s += outline(it, nivel + 1) }
        return s
    }
}

O diretório de bibliotecas é configurável em Preferências → Scripts. Uma vez apontado, os scripts passam a poder usar Utilidades.outline(node) sem repetir o código.

💡

Versione a pasta scripts/ e lib/ num repositório Git. Além do histórico, é o que permite levar a sua biblioteca para outra máquina com um git clone — e é o passo anterior a empacotá-la como add-on.

Instalando como item de menu🔗

Um script salvo em scripts/ aparece automaticamente em Ferramentas → Scripts depois de reiniciar o Freeplane. Para dar-lhe atalho e posição de menu próprios, o caminho é declará-lo num add-on — que é, ele mesmo, um mapa com uma seção scripts contendo menuLocation, keyboardShortcut e executionMode.

A executionMode merece atenção: ON_SINGLE_NODE roda uma vez com node sendo o nó selecionado, enquanto ON_SELECTED_NODE roda uma vez por nó selecionado. Escolher errado faz um script de relatório abrir dez caixas de mensagem.

Erros comuns

  • Rodar num mapa de trabalho sem testar em cópia — o desfazer de operações em massa não é confiável
  • Escrever scripts não idempotentes, que corrompem o mapa quando rodados duas vezes
  • Apagar em vez de marcar: um script de faxina que apaga é um script que você vai temer usar
  • Conceder permissão de escrita em arquivos e nunca mais revisá-la
  • Confundir ON_SINGLE_NODE com ON_SELECTED_NODE e multiplicar a execução por seleção
  • Repetir o mesmo trecho em seis scripts em vez de extraí-lo para lib/

Você aprendeu

  • Script vale a pena quando a operação é repetitiva, mecânica e propensa a erro
  • Idempotência não é refinamento: é o que impede estrago quando alguém roda de novo
  • Marcar em vez de apagar mantém a decisão com o usuário
  • lib/ mais o diretório de bibliotecas nas preferências transforma scripts soltos em biblioteca
  • Versionar a pasta de scripts é o passo anterior a empacotar como add-on
  • executionMode decide se o script roda uma vez ou uma vez por nó selecionado

Perguntas para reflexão

  1. O que torna o script de numeração idempotente, e por que isso importa?
  2. Por que um script de faxina deve marcar em vez de apagar?
  3. Qual é o idioma Groovy para recursão com closure, e por que a forma direta não funciona?
  4. Que problema o relatório de atributos ausentes detecta antes que ele apareça num filtro?
  5. Qual a diferença entre ON_SINGLE_NODE e ON_SELECTED_NODE?
  6. Como se transforma um conjunto de scripts soltos numa biblioteca reutilizável?

Artigos relacionados

Referências🔗

  1. Freeplane — Scripting basics
  2. Freeplane — Scripting API
  3. Freeplane — Scripting cheatsheet
  4. Freeplane — Your own utility script library
  5. Freeplane — Scripts collection
  6. Freeplane — Add-ons (develop)
  7. Freeplane — Security considerations
  8. Apache Groovy — documentação