├── README.md ├── add_title ├── backtick ├── convert ├── convert-links.el ├── delete ├── headings ├── links ├── logseq-migration ├── logseq-migration.el ├── namespaces ├── pandoc-cmd ├── postproc ├── preproc ├── properties ├── remove-custom-ids.el └── remove-logseq-property-entries.el /README.md: -------------------------------------------------------------------------------- 1 | This is a set of rough scripts that I used to help me convert my Logseq graph to org-roam. Use at your own risk, and please read the code before you use it. 2 | 3 | As I no longer have any use for this code (having finished converting my Logseq graph), I will not be maintaining it in any way. If you need any improvements, you'll have to fork the repo and work on them yourself. 4 | 5 | # Supported 6 | - conversion of Logseq's weird markdown dialect to org-mode: pandoc does most of the actual conversion, but various scripts are needed to massage Logseq's markdown into something it can understand 7 | - converting links: Logseq links/tags are converted to org-roam links; page aliases are supported 8 | 9 | # Not Supported 10 | - queries and embeds 11 | - images and other file assets 12 | - journals (see Instructions below) 13 | - many other things i didn't think of, no doubt 14 | 15 | # Requirements 16 | Tested with: 17 | 18 | ``` 19 | pandoc 3.1.9 20 | emacs 29.1 21 | ``` 22 | In theory newer versions should work. 23 | 24 | # Instructions 25 | - Clone this repo. 26 | - Backup your existing logseq graph folder, just in case. 27 | - These scripts are designed to run on the 'pages' folder of your graph; decide what you're going to do with your journals. I combined them all into a single page like this: 28 | ``` sh 29 | for file in journals/*; do 30 | cat "$file" >> pages/journals.md 31 | done 32 | ``` 33 | - run the shell script `logseq-migration` on your graph's `pages` folder: 34 | `path/to/this/repo/logseq-migration pages` 35 | This will create a folder named `pages_` containing converted org-mode files; see the comments in `logseq-migration` for details. 36 | - in Emacs, run the code in `logseq-migration.el`. This will convert links and do some other post-processing. 37 | -------------------------------------------------------------------------------- /add_title: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | graphdir=$1 4 | pagepath=$(sed 's/^\///' <<<"${2#"$graphdir"}") 5 | title=${pagepath%.org} 6 | 7 | echo "#+title: $title" > "$2".temp 8 | cat "$2" >> "$2".temp 9 | mv "$2".temp "$2" 10 | -------------------------------------------------------------------------------- /backtick: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Enclose Logseq block ids and block embeds in backticks. 3 | # This is so pandoc will turn them into Org-mode 'verbatim' notation (eg. 4 | # =foo=), which we can then process with elisp. 5 | 6 | # set -x 7 | IFS=$'\n' 8 | 9 | # Match Logseq IDs (extended regex) 10 | id_eregex='[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' 11 | 12 | # Match the targets of embed/query syntax (extended regex) 13 | target_eregex='([0-9a-f]|\(|\)|\[|\]|-)+' 14 | 15 | # block ids 16 | sed -E -i 's/id:: '"$id_eregex"'/`\0`/' "$1" 17 | # embeds 18 | sed -E -i 's/\{\{ *embed '"$target_eregex"' *\}\}/`\0`/' "$1" 19 | # queries 20 | sed -i 's/{{ *query.*}}/`\0`/' "$1" 21 | -------------------------------------------------------------------------------- /convert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=~/bin/scripts/logseq-migration 4 | 5 | find "${1:-.}" -wholename "*.md" -exec "$DIR"/pandoc-cmd '{}' \; -exec rm '{}' \; 6 | -------------------------------------------------------------------------------- /convert-links.el: -------------------------------------------------------------------------------- 1 | ;;; convert-links.el --- convert logseq links to org-id links -*- lexical-binding: t; -*- 2 | 3 | (setq logseq/id-regexp "[0-9a-f]\\{8\\}-\\(?:[0-9a-f]\\{4\\}-\\)\\{3\\}[0-9a-f]\\{12\\}") 4 | (setq logseq/id-spec-regexp (concat "=id:: " logseq/id-regexp "=\\( \\|\n\\)")) 5 | (setq logseq/filelink-target-regexp "[^]]*") 6 | (setq logseq/alias-regexp "^alias:: \\(.*?\\)$") 7 | 8 | (defun logseq/--get-id-part (match) 9 | ;; check if it's an id link 10 | (if (string-match logseq/id-regexp match) 11 | (substring-no-properties (match-string 0 match)) 12 | ;; if not, check if it's a file link 13 | (when (string-match logseq/filelink-regexp match) 14 | (substring-no-properties (match-string 1 match))))) 15 | 16 | (defun logseq/--convert-id-links-in-file (file hmap) 17 | "Generate org-ids in FILE, and return the association with their contexts. 18 | First, convert the file itself into an org-roam node; then, remove logseq 19 | 'id::'s and convert the headlines they apply to into org-roam nodes (by 20 | assigning an org id). 21 | Add the associations to HMAP. For file nodes, associate the page title with 22 | the node's id. For headline nodes, associate the replaced logseq id with the 23 | node id." 24 | (find-file file) 25 | (goto-char 0) 26 | ;; convert the file itself into a node 27 | (re-search-forward "#\\+title: \\(.*\\)$") 28 | (let* ((title (substring-no-properties (match-string 1))) 29 | (id (org-id-get-create))) 30 | (puthash title id hmap) 31 | (if (re-search-forward logseq/alias-regexp nil t) 32 | (let ((aliases (split-string (match-string-no-properties 1) ", *"))) 33 | (message "%s" aliases) 34 | (dolist (alias aliases) (puthash alias id hmap)))) 35 | ;; search for logseq ids 36 | (while (re-search-forward logseq/id-spec-regexp nil t) 37 | (let ((match (substring-no-properties (match-string 0)))) 38 | ;; delete logseq id 39 | (replace-match "" nil nil) 40 | (let ((id-part (logseq/--get-id-part match))) 41 | ;; key is the old logseq id; value is newly created org id for this entry 42 | (puthash id-part (org-id-get-create) hmap))))) 43 | (save-buffer) 44 | hmap) 45 | 46 | (defun logseq/convert-id-links (graphdir) 47 | ;; (eq "a" "a") -> nil; (eql "a" "a") -> nil; (equal "a" "a") -> t 48 | (let ((hmap (make-hash-table :test 'equal))) 49 | (dolist (file (directory-files-recursively graphdir "org$")) 50 | (logseq/--convert-id-links-in-file file hmap)) 51 | hmap)) 52 | 53 | (setq logseq/embed-regexp (concat "={{ *\\(embed\\) " logseq/id-regexp " *}}=")) 54 | (setq logseq/query-regexp (concat "={{ *\\(query\\) " logseq/id-regexp " *}}=")) 55 | (setq logseq/link-regexp (concat "\\[\\[file:((" logseq/id-regexp "))\\]\\[\\(.*\\)\\]\\]")) 56 | (setq logseq/filelink-regexp 57 | (concat "\\[\\[file:\\(" logseq/filelink-target-regexp "\\)\\]\\]")) 58 | (setq logseq/blockref-regexp (concat "((" logseq/id-regexp "))")) 59 | 60 | (defun logseq/--replace-with-org-links-in-file (file hmap) 61 | "Replace Logseq link syntax in FILE with org-id links, based on the 62 | associations in HMAP." 63 | (find-file file) 64 | (goto-char 0) 65 | (while (or (re-search-forward logseq/embed-regexp nil t) 66 | (re-search-forward logseq/query-regexp nil t) 67 | (re-search-forward logseq/link-regexp nil t) 68 | (re-search-forward logseq/filelink-regexp nil t) 69 | (re-search-forward logseq/blockref-regexp nil t)) 70 | (let* ((match (substring-no-properties (match-string 0))) 71 | (match-data (match-data)) 72 | (id-part (logseq/--get-id-part match)) 73 | (node-struct (org-roam-node-from-id 74 | (gethash id-part hmap)))) 75 | (set-match-data match-data) 76 | (when node-struct 77 | ;; HACK: prevent org-roam-node-insert from reading a node from 78 | ;; user; instead just use our node 79 | (replace-match "" nil nil) 80 | (cl-letf (((symbol-function 'org-roam-node-read) 81 | (lambda (&rest args) node-struct))) 82 | ;; ((symbol-function 'org-roam-node-formatted) 83 | ;; (lambda (node) (org-roam-node-title node)))) 84 | (org-roam-node-insert))) 85 | (set-match-data match-data) 86 | (unless node-struct 87 | (replace-match (substring-no-properties 88 | (or (match-string 1) "")) nil nil) 89 | (message (format "No node found for %s" match)))) 90 | (goto-char 0)) 91 | (save-buffer)) 92 | 93 | (defun logseq/replace-with-org-links (graphdir hmap) 94 | (dolist (file (directory-files-recursively graphdir "org$")) 95 | (logseq/--replace-with-org-links-in-file file hmap))) 96 | 97 | ;; (let* ((file "~/scratchdir/logseq_main_/pages/linux/arch/installation.org") 98 | ;; (table (logseq/--convert-id-links-in-file file (make-hash-table)))) 99 | ;; (org-roam-db-sync t) 100 | ;; (logseq/--replace-with-org-links-in-file file table)) 101 | 102 | (defun logseq/convert-links (graphdir) 103 | (let ((table (logseq/convert-id-links graphdir)) 104 | (org-roam-directory graphdir)) 105 | (org-roam-db-sync t) 106 | (logseq/replace-with-org-links graphdir table))) 107 | -------------------------------------------------------------------------------- /delete: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete 'collapsed:: ' 4 | sed -i 's/collapsed:: .*//' "$1" 5 | -------------------------------------------------------------------------------- /headings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Convert nested lists to #, ##, ###... so that pandoc's Org converter will turn 3 | # them into nested headings. 4 | 5 | perl -pi -e ' 6 | # Add an additional layer of nesting - all text in org files should be under a 7 | # heading 8 | s/^/ /; 9 | # Discard any existing Markdown header syntax ("#" characters after the list bullet) 10 | s/( *- )#* /$1/; 11 | # For each level of indentation, add a "#" 12 | s/ (?= *-)/#/g; 13 | # Finally, remove list bullets 14 | s/^(#*)-/$1/; 15 | # Remove any Tab characters remaining (eg. they will still be present in code 16 | # blocks) 17 | s/^ +//' "$1" 18 | 19 | # Add newlines between headings - apparently this is required by Markdown syntax 20 | sed -i -E 's/^#/\'$'\n#/' "$1" 21 | -------------------------------------------------------------------------------- /links: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Convert Logseq link syntax into standard Markdown link syntax. 3 | 4 | # Changing the path of links to the assets folder - if you don't know why you'd 5 | # need this, then comment it out 6 | sed -i 's/\[\.\.\/assets/\[assets/g' "$1" 7 | 8 | # Convert '[[pagename]]' links 9 | sed -E -i 's/\[\[(.*)\]\]/[\1](\1)/g' "$1" 10 | 11 | # Convert '#pagename' links 12 | sed -E -i '/```/,/```/ !s/ #([^ ]+)/ [\1](\1)/g' "$1" 13 | # ('!' inverts range - everywhere except in code blocks. We need this because 14 | # code comments may begin with '#') 15 | -------------------------------------------------------------------------------- /logseq-migration: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Main script, used to convert all the files in a directory to org files. Call 4 | # it with the directory as argument: for example, `./logseq-migration pages`. 5 | # This will create a copy of `pages` called `pages_`, and the conversion will be 6 | # applied to all files in `pages`. Then it will copy `pages_` to `pages__`, 7 | # which will serve as a backup for when you do further processing on `pages_`. 8 | 9 | export scriptdir=~/bin/scripts/logseq-migration 10 | 11 | rm -r "$1"_ "$1"__ 12 | cp -r "$1" "$1"_ 13 | "$scriptdir"/preproc "$1"_ 14 | "$scriptdir"/convert "$1"_ 15 | "$scriptdir"/postproc "$1"_ 16 | cp -r "$1"_ "$1"__ 17 | -------------------------------------------------------------------------------- /logseq-migration.el: -------------------------------------------------------------------------------- 1 | ;;; logseq-migration.el --- elisp processing of converted logseq graph -*- lexical-binding: t; -*- 2 | 3 | (load-file "convert-links.el") 4 | (load-file "remove-custom-ids.el") 5 | (load-file "remove-logseq-property-entries.el") 6 | (let ((graphdir "path/to/my/graph/pages_")) ;; change as needed 7 | (logseq/remove-custom-ids graphdir) 8 | (logseq/convert-links graphdir) 9 | (logseq/remove-logseq-property-entries graphdir)) 10 | ;; comment out the below line if you don't want to delete logseq properties 11 | -------------------------------------------------------------------------------- /namespaces: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Represent Logseq's 'namespaces' by moving the page files into directories. 3 | # For example, a page named 'a/b/c' in Logseq, whose file is named 'a___b___c', 4 | # will stored as 'c' in the path 'a/b'. 5 | 6 | # if the page is not under a namespace, will return the same filename 7 | path=$(sed 's/___/\//g' <<<"$1") 8 | 9 | dir=${path%/*.*} 10 | 11 | # echo $PWD/$dir 12 | # echo $PWD/$path 13 | mkdir -p "$PWD/$dir" 14 | 15 | old=$PWD/$1 16 | new=$PWD/$path 17 | 18 | # to avoid same-file errors, run `mv` only if the file would actually be moved 19 | # (if it was not under a namespace, $old and $new are the same thing) 20 | [[ $(realpath "$old") = "$(realpath "$new")" ]] || mv "$old" "$new" 21 | -------------------------------------------------------------------------------- /pandoc-cmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Call pandoc on a .md file to produce a .org file 3 | 4 | pandoc --wrap=none -f markdown -t org -o "${1%.*}.org" "$1" 5 | -------------------------------------------------------------------------------- /postproc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scriptdir=~/bin/scripts/logseq-migration 4 | 5 | find "${1:-.}" -wholename "*.org" -exec "$scriptdir"/add_title "$1" '{}' \; 6 | -------------------------------------------------------------------------------- /preproc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Overall preprocessing of Logseq markdown before converting to org 3 | 4 | scriptdir=~/bin/scripts/logseq-migration 5 | 6 | find "${1:-.}" -wholename "*.md" -exec "$scriptdir"/properties '{}' \; -exec "$scriptdir"/backtick '{}' \; -exec "$scriptdir"/delete '{}' \; -exec "$scriptdir"/headings '{}' \; -exec "$scriptdir"/links '{}' \; -exec "$scriptdir"/namespaces '{}' \; 7 | -------------------------------------------------------------------------------- /properties: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Move any property into a block, otherwise the pandoc org parser merges them 4 | # all into a single line for some reason 5 | sed -E -i 's/^[a-z]+::.*$/- !property-deleteme!\n \0/' "$1" 6 | -------------------------------------------------------------------------------- /remove-custom-ids.el: -------------------------------------------------------------------------------- 1 | ;;; remove-custom-ids.el --- Remove CUSTOM_ID property assigned by pandoc org parser -*- lexical-binding: t; -*- 2 | 3 | (defun logseq/remove-custom-ids (graphdir) 4 | (dolist (file (directory-files-recursively graphdir "org$")) 5 | (with-temp-file file 6 | (insert-file-contents file) 7 | (org-delete-property-globally "CUSTOM_ID")))) 8 | -------------------------------------------------------------------------------- /remove-logseq-property-entries.el: -------------------------------------------------------------------------------- 1 | ;;; remove-logseq-property-entries.el --- Remove logseq property subtrees -*- lexical-binding: t; -*- 2 | 3 | ;; this will write to your kill ring! 4 | (defun logseq/remove-logseq-property-entries (graphdir) 5 | "In the 'properties' shell script, we moved each logseq property into a block 6 | to prevent pandoc's org parser from messing them up. Now that we don't need the 7 | properties anymore, we delete them." 8 | (org-map-entries #'org-cut-subtree 9 | "!property-deleteme!" 10 | (directory-files-recursively graphdir "org$"))) 11 | --------------------------------------------------------------------------------