Org Roam

Notas tomadas con Emacs y Org-Roam. Exportadas con ox-hugo a Markdown.

2024

Pruebas anterior con metaprogramación

Primero creé varios scripts para automatizar la creación de una web a partir de las notas. Debe seguir publicado en esta dirección.

#!/bin/bash
. ./.private

# 0. Definición variables
# ------------------------------------------------------------------------------
ORG_ROAM_ORIGINAL_FOLDER=~/Documents/org-roam-files # Ubicación de mis notas
ORG_ROAM_HTML_FILES_FOLDER=orgroam-html # Donde están los html generados por build-orgroam-with-emacs.sh. Esta variable se usa en build-site.el
# Donde guardo de forma plana todos los archivos para poder usarlo en python para extraer abstract.
# TODO: a eliminar porque debemos poder extraer del html generado, lo que nos evita mucho trabajo de expresión regular en python.
ORG_ROAM_RAW_FILES_FOLDER=orgroam-raw
IMAGES_FOLDER=posts/img
PROJECT_FOLDER=~/projects/blogs/qoback-orgroam

# 1. Borramos todo lo antiguo. Más bonito si controlase fechas y copiase solo lo necesario, pero innecesario.
# ------------------------------------------------------------------------------
rm posts/*.html
rm -r $ORG_ROAM_HTML_FILES_FOLDER/**/*.html
rm $ORG_ROAM_RAW_FILES_FOLDER/*.org

# 2. Copio mis notas salvo las que son privadas
# ------------------------------------------------------------------------------
find $ORG_ROAM_ORIGINAL_FOLDER -type d \( -path $ORG_ROAM_ORIGINAL_FOLDER/home -o -path $ORG_ROAM_ORIGINAL_FOLDER/fitosat \) -prune -o -name '*.org' -print | xargs -I% cp % $ORG_ROAM_RAW_FILES_FOLDER

# 3. Construyo html a partir de org. Se guarda la misma estructura de carpetas
# ------------------------------------------------------------------------------
emacs -Q --script build-site.el
$PROJECT_FOLDER/create_graph.py $ORG_ROAM_ORIGINAL_FOLDER
# rm -rf ~/projects/blogs/qoback/$ORG_ROAM_HTML_FILES_FOLDER/home
# rm -rf ~/projects/blogs/qoback/$ORG_ROAM_HTML_FILES_FOLDER/fitosat
find $ORG_ROAM_ORIGINAL_FOLDER -type f -name "*.png" -o -name "*.jpg" | xargs -I% cp % $IMAGES_FOLDER

# 4. Creo html de cada poast
# ------------------------------------------------------------------------------
echo ""
echo "==> Creando Posts para publicación"
for file in $(find $ORG_ROAM_HTML_FILES_FOLDER -type d \( -path $ORG_ROAM_HTML_FILES_FOLDER/home -o -path $ORG_ROAM_HTML_FILES_FOLDER/fitosat \) -prune -o -name "*.html" -print); do
    echo "        $file"
    ./insert_raw_post_in_layout.sh layout/main.html $file
done

# TODO: mezclar y que solamente haya un listado con abstract y grafo
# 5. Creo index.html con previas
# ------------------------------------------------------------------------------
echo ""
echo "==> Creando índice"
./create_index_page.py posts

# 6. Creo posts/index.html con grafo de dependencias
# ------------------------------------------------------------------------------
./create_knwoledge_page.py


# 7. [Opcional] Desplegamos
# ------------------------------------------------------------------------------
if [[ $1 == 'up' ]]; then
    ip=${host[ip]}
    port=${host[port]}
    user=${host[user]}
    rsync -avzP -e "ssh -p $port" --exclude-from="exclude.txt" . $user@$ip:$dst
fi

Script para crear el grafo que mostrar más tarde con echarts.

#!/usr/bin/python

import os
import sys
import re
import json

path_to_org_files = sys.argv[1]
connections = {}
conns = []
categories = {}
files = os.listdir(path_to_org_files)
max_length = max([len(f) for f in files])

for dirpath, dirnames, filenames in os.walk(path_to_org_files):
        for file in filenames:
            if file.endswith('.org'):
                with open(os.path.join(dirpath, file), 'r', encoding="utf-8") as f:
                    content = f.read()
                    match_id = re.search(r':ID:\s*([-\w]+)', content)
                    match_name = re.search(r'#\+TITLE:\s*(.+)', content)

                if match_id:
                    current_id = match_id.group(1)
                    current_name = match_name.group(1).strip() if match_name else current_id
                    conns.append({
                        "id": current_id,
                        'name': current_name,
                        'links':  list(set(re.findall(r'\[\[id:([-\w]+)\]', content))),
                        'file': file,
                        'category': None
                    })

conns = sorted(conns, key=lambda e: len(e['links']), reverse=True)

for idx, conn in enumerate(conns):
    connections[conn['id']] = conn
    category = idx if categories.get(conn['id'], None) is None else categories[conn['id']]

    if not len(conn['links']):
        categories[conn['id']] = category
        print(f"{conn['name']:<{max_length}} [{category:>2}]")
    for link in conn['links']:
        print(f"{conn['name']:<{max_length}} [{category:>2}] -> {link}")
        categories[link] = category

    connections[conn['id']]['category'] = category

json_data = json.dumps(connections, indent=4)

with open('posts/connections.json', 'w') as f:
    f.write(json_data)

print("\n==> JSON creado")

Post para montara cada post, insert_raw_post_in_layout.sh.

[[ $# != 2 ]] && echo "HTML layout and raw post HTML must be provided." && exit -1

function replace_text {
    file_path=$1
    sed -i 's/Table of Contents/Índice/g' $file_path
    sed -i 's/Footnotes:/Notas/g' $file_path
}

# TODO: This must be seteable from command line with extra key arguments.
INSERT_STYLE=0  # --ijs 1
INSERT_SCRIPT=0  # --icss 1

LAYOUT_FILE=$1
RAW_HTML_PATH=$2

LAYOUT_FILE=layout/main.html

RAW_HTML_FILE=${RAW_HTML_PATH##*/}

NL_BODY=$(grep -n %INSERT% $LAYOUT_FILE | cut -d':' -f 1)
NL_STYLE=$(grep -n %STYLE-SCRIPT% $LAYOUT_FILE | cut -d':' -f 1)

DST_FOLDER=posts
DST_PATH=$DST_FOLDER/$RAW_HTML_FILE

mkdir -p $DST_FOLDER

BODY=$(xmllint --html --xpath "//body//div[@id='content']" $RAW_HTML_PATH)

if [[ $INSERT_STYLE == 1 ]]; then
    STYLE=$(xmllint --html --xpath "string(//style)" $RAW_HTML_PATH)
fi

if [[ $INSERT_SCRIPT == 1 ]]; then
    SCRIPT=$(xmllint --html --xpath "string(//script)" $RAW_HTML_PATH)
fi

{
    head -n $NL_STYLE $LAYOUT_FILE

    [[ $INSERT_STYLE == 1 ]] && echo "<style type=\"text/css\">" && echo "$STYLE" && echo "</style>"
    [[ $INSERT_SCRIPT == 1 ]] && echo "<script type=\"text/javascript\">" && echo "$SCRIPT" && echo "</script>"

    tail -n +$NL_STYLE $LAYOUT_FILE | head -n $((NL_BODY-NL_STYLE))
    echo "$BODY"
    tail -n +$NL_BODY $LAYOUT_FILE
} > $DST_PATH

replace_text $DST_PATH

Con python creamos el index.html, con el archivo create_index_page.py

#!/usr/bin/python

import os
import sys
import re
import json

import sys


html_files_path = sys.argv[1]
files = os.listdir(html_files_path)
max_length = max([len(f) for f in files])

pre = """
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Qoback Roam</title>
    <link rel="stylesheet" href="/css/posts.css" />
    <link rel="stylesheet" href="/css/styles.css" />
    <link rel="stylesheet" href="/css/org-styles.css" />
    <link rel="stylesheet" href="/css/simple.css" />
    <link rel="stylesheet" href="/css/post.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <!-- <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" /> -->
    <link
      href="https://fonts.googleapis.com/css2?family=Teko&display=swap"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=Spline+Sans+Mono:ital,wght@0,300;0,400;0,600;1,400&display=swap"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=Akshar:wght@300;400;600&display=swap"
      rel="stylesheet"
    />
    <!-- %STYLE-SCRIPT% -->
  </head>
  <body>
    <nav>
      <a href="/"><div>Home</div></a>
      <a href="/posts/"><div>Roam</div></a>
    </nav>

    <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
    <script
      id="MathJax-script"
      async
      src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
      ></script>

    <input type='text'/>
    <div id='content' class='posts'>
"""

post = """
    </div>
  </body>
</html>
"""

with open("index.html", "w") as writer:
    writer.write(pre)
    for dirpath, dirnames, filenames in os.walk(html_files_path):
        for file in filenames:
            # print(dirpath, dirnames, file)
            base_name = file[:-5]
            if not file.endswith(".html") or file == "index.html":
                continue
            try:
                with open(f"orgroam-raw/{base_name}.org", "r") as org_reader:
                    content = org_reader.read()
                    match_created = re.search(r"#\+CREATED:\s*\[(.+)\]", content)
                    match_updated = re.search(r"#\+LAST_MODIFIED:\s*\[(.+)\]", content)

                    with open(f"{dirpath}/{file}", "r") as org_reader:
                        c = org_reader.read()
                        abstract = re.search(r"(<p>.*?</p>)", c, re.DOTALL).group(1)

                    title = re.sub("[-_]", " ", base_name)
                    card = f"""
                    <div class='wrapper'>
                      <div class='preview'>
                        <a class=\"title\" href='/posts/{base_name}.html'>
                          {title}
                        </a>
                        </br>
                        <div style="display: flex; justify-content: space-between;">
                          <sup>{match_created.group(1)}</sup>
                          <sup>{match_updated.group(1)}</sup>
                        </div>
                      {abstract}
                      </div>
                    </div>"""
                    writer.write(card)
            except:
                print(f"\t ** Passing though file {file} **")

        writer.write(post)

Finalmente creamos el index.html donde se muestra el grafo creado con python usando echarts.

#!/usr/bin/python
import os
import re

# <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />

print("\n\n==>> Creating post/index.html")


pre = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Qoback Blog</title>
<link rel="stylesheet" href="/css/posts.css" />
<link rel="stylesheet" href="/css/styles.css" />
<link rel="stylesheet" href="/css/org-styles.css" />
<link rel="stylesheet" href="/css/simple.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Teko&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Spline+Sans+Mono:ital,wght@0,300;0,400;0,600;1,400&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Akshar:wght@300;400;600&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.2.2/echarts.min.js"></script>

</head>
<body>
    <nav>
      <a href="/"><div>Home</div></a>
      <a href="/posts/"><div>Roam</div></a>
    </nav>

    <div id="main" style="width: 100%;height:40vh;"></div>

    <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
    <script
    id="MathJax-script"
    async
    src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
    ></script>
    <script>
    fetch('connections.json')
    .then(response => response.json())
    .then(data => {
        let nodes = [];
        let links = [];
        let uniqueColors = new Set();  // Guardar los colores únicos para definir la paleta de colores

        for (let id in data) {
                nodes.push({
                    id: id,
                    name: data[id].name,
                    category: data[id].category,
                    itemStyle: {
                        color: `hsl(${data[id].category * 50 % 360}, 70%, 60%)` // Generar color basado en el valor
                    }
                });
                uniqueColors.add(data[id].category);
                data[id].links.forEach(link => {
                    links.push({
                        source: id,
                        target: link
                    });
                });
        }


        const option = {
            series: [{
                animation: false,
                categories: [],
                focusNodeAdjacency: true,
                itemStyle: {
                    borderColor: '#fff',
                    borderWidth: 1,
                    shadowBlur: 10,
                    shadowColor: 'rgba(0, 0, 0, 0.3)'
                },
                draggable: true,
                type: 'graph',
                layout: 'force',
                data: nodes,
                links: links,
                roam: true,
                label: {
                    position: 'right',
                    formatter: '{b}'
                },
                lineStyle: {
                    color: 'source',
                    curveness: 0.3
                },
                emphasis: {
                    focus: 'adjacency',
                    lineStyle: {
                        width: 3
                    }
                }
            }]
        };

        let chart = echarts.init(document.getElementById('main'));
        chart.setOption(option);

        chart.on('click', function(params) {
            if (params.dataType === 'node') {
                    const nodeId = params.data.id;
                    //const filePath = `posts/${data[nodeId].file.slice(0, -3)}html`;
                    const filePath = `${data[nodeId].file.slice(0, -3)}html`;
                    window.open(filePath, '_blank');
            }
        });
    });

    </script>
    """


def create_body(path_to_html_files):
    pattern = "[-_]"
    body = """
    <input type='text'/>
    <div id='content' class='posts'>"""

    for _, _, filenames in os.walk(path_to_html_files):
        for file in filenames:
            if file != "index.html" and file.endswith(".html"):
                print(f"\tCreating post item for file {file}")
                body += f"""
                <div class='wrapper'>
                <div class='preview'>
                <a class=\"title\" href='/posts/{file}'>
                {re.sub(pattern, ' ', file[:-5])}
                </a>
                <br>
                </div>
      </div>
                """

    return body


post = """
  </body>
</html>
"""


with open("posts/index.html", "w") as f:
    f.write(pre)
    f.write(create_body("posts"))
    f.write(post)