branch: externals/org-gnosis commit 3a03db1c16ceb098bd892b0e0eaa6be995830696 Author: Thanos Apollo <pub...@thanosapollo.org> Commit: Thanos Apollo <pub...@thanosapollo.org>
[fix] Refactor buffer data parsing. + Incldue fixes for edge case scenarios. --- org-gnosis.el | 161 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 103 insertions(+), 58 deletions(-) diff --git a/org-gnosis.el b/org-gnosis.el index ec12009dc1..2153d96666 100644 --- a/org-gnosis.el +++ b/org-gnosis.el @@ -199,13 +199,20 @@ to have an ID." (while (re-search-forward org-link-any-re end t) (let ((link (match-string-no-properties 0))) (when (string-match "id:\\([^]]+\\)" link) - (push (cons (match-string 1 link) (org-gnosis-get-id)) links))))) + (let ((target-id (match-string 1 link)) + (source-id (save-excursion + (org-back-to-heading-or-point-min t) + (org-id-get)))) + (when (and target-id source-id) + (push (cons target-id source-id) links))))))) (nreverse links))) (defun org-gnosis-get-data--topic (&optional parsed-data) - "Retrieve the title and ID from the current org buffer or given PARSED-DATA." - (let* ((parsed-data (or parsed-data (org-element-parse-buffer))) - (id (org-element-map parsed-data 'property-drawer + "Retrieve the title and ID from the current org buffer or given PARSED-DATA. +Returns (title tags id) or signals error if required data is missing." + (unless parsed-data + (setq parsed-data (org-element-parse-buffer))) + (let* ((id (org-element-map parsed-data 'property-drawer (lambda (drawer) (org-element-map (org-element-contents drawer) 'node-property (lambda (prop) @@ -213,26 +220,30 @@ to have an ID." (org-element-property :value prop))) nil t)) nil t)) - (title (org-gnosis-adjust-title - (org-element-map parsed-data 'keyword - (lambda (kw) - (when (string= (org-element-property :key kw) "TITLE") - (org-element-property :value kw))) - nil t) - id)) - (tags (org-gnosis-get-filetags))) + (title-raw (org-element-map parsed-data 'keyword + (lambda (kw) + (when (string= (org-element-property :key kw) "TITLE") + (org-element-property :value kw))) + nil t)) + (title (when title-raw (org-gnosis-adjust-title title-raw id))) + (tags (org-gnosis-get-filetags parsed-data))) + ;; Validate required fields + (unless id + (error "Org buffer must have an ID property")) + (unless (and title (not (string-empty-p title))) + (error "Org buffer must have a non-empty TITLE")) (list title tags id))) -;; This one is used for topics (defun org-gnosis-get-filetags (&optional parsed-data) - "Return the filetags of the buffer's PARSED-DATA as a comma-separated string." + "Return the filetags of the buffer's PARSED-DATA as a list of strings." (let* ((parsed-data (or parsed-data (org-element-parse-buffer))) (filetags (org-element-map parsed-data 'keyword (lambda (kw) (when (string-equal (org-element-property :key kw) "FILETAGS") (org-element-property :value kw))) nil t))) - (and filetags (remove "" (split-string filetags ":"))))) + (when (and filetags (not (string-empty-p (string-trim filetags)))) + (remove "" (split-string filetags ":"))))) (defun org-gnosis-parse-topic (parsed-data) "Parse topic information from the PARSED-DATA." @@ -247,18 +258,36 @@ to have an ID." (defun org-gnosis-buffer-data (&optional data) "Parse DATA in current buffer for topics & headlines with their ID, tags, links." (let* ((parsed-data (or data (org-element-parse-buffer))) - (topic (org-gnosis-parse-topic parsed-data)) - (all-ids (when topic (list (plist-get topic :id)))) - (inherited-tags (plist-get topic :tags)) - (headlines '())) - (org-element-map parsed-data 'headline - (lambda (headline) - (let ((parsed-headline (org-gnosis-parse-headline - headline inherited-tags (plist-get topic :id)))) - (when parsed-headline - (push parsed-headline headlines) - (push (plist-get parsed-headline :id) all-ids))))) - (nreverse (cons topic headlines)))) + (topic (org-gnosis-parse-topic parsed-data))) + (unless topic (error "Buffer must have a topic with ID")) + (let ((headlines '()) + (tag-stack (list (plist-get topic :tags))) + (id-stack (list (plist-get topic :id))) + (topic-id (plist-get topic :id))) + (org-element-map parsed-data 'headline + (lambda (headline) + (let* ((level (org-element-property :level headline)) + (headline-tags (org-element-property :tags headline)) + (current-id (org-element-property :ID headline))) + ;; Adjust stacks to proper level + (while (>= (length id-stack) level) + (pop id-stack)) + (while (>= (length tag-stack) level) + (pop tag-stack)) + ;; Calculate combined tags + (let* ((inherited-tags (or (car tag-stack) '())) + (combined-tags (org-gnosis--combine-tags inherited-tags headline-tags))) + ;; Calculate master ID from current stack state + (let ((master-id (when current-id + (org-gnosis--find-master-id id-stack level topic-id)))) + ;; Push current values to stacks for children + (push current-id id-stack) + (push combined-tags tag-stack) + ;; Only parse headlines with IDs + (when current-id + (when-let* ((parsed (org-gnosis-parse-headline headline combined-tags master-id))) + (push parsed headlines)))))))) + (nreverse (cons topic headlines))))) (defun org-gnosis-get-file-info (filename) "Get data for FILENAME. @@ -276,32 +305,43 @@ Returns file data with FILENAME." "Add contents of FILE to database. If JOURNAL is non-nil, update file as a journal entry." - (let* ((info (org-gnosis-get-file-info file)) - (data (butlast info)) - (table (if journal 'journal 'nodes)) - (filename (file-name-nondirectory file)) - (links (and (> (length info) 1) (apply #'append (last info)))) - (titles (org-gnosis-select 'title table nil t))) - ;; Add gnosis topic - (message "Parsing: %s" filename) - (cl-loop for item in data - do (let ((title (org-gnosis-adjust-title - (plist-get item :title))) - (id (plist-get item :id)) - (master (plist-get item :master)) - (tags (plist-get item :tags)) - (level (plist-get item :level))) - (when (member title titles) - (error "Title: '%s' already exists" title)) - (org-gnosis--insert-into table `([,id ,filename ,title ,level ,tags])) - (cl-loop for tag in tags - do - (org-gnosis--insert-into 'tags `([,tag])) - (org-gnosis--insert-into 'node-tag `([,id ,tag])) - (when (stringp master) - (org-gnosis--insert-into 'links `([,id ,master])))))) - (cl-loop for link in links - do (org-gnosis--insert-into 'links `[,(cdr link) ,(car link)])))) + (condition-case err + (let* ((info (org-gnosis-get-file-info file)) + (data (butlast info)) + (table (if journal 'journal 'nodes)) + (filename (file-name-nondirectory file)) + (links (and (> (length info) 1) (apply #'append (last info)))) + (titles (org-gnosis-select 'title table nil t))) + ;; Add gnosis topic and nodes + (message "Parsing: %s" filename) + (emacsql-with-transaction org-gnosis-db + (cl-loop for item in data + do (let ((title (org-gnosis-adjust-title + (plist-get item :title))) + (id (plist-get item :id)) + (master (plist-get item :master)) + (tags (plist-get item :tags)) + (level (plist-get item :level))) + ;; Handle duplicate titles gracefully + (when (member title titles) + (let ((counter 1)) + (while (member (format "%s (%d)" title counter) titles) + (setq counter (1+ counter))) + (setq title (format "%s (%d)" title counter)) + (message "Duplicate title found, renamed to: %s" title))) + (org-gnosis--insert-into table `([,id ,filename ,title ,level ,tags])) + ;; Insert tags + (cl-loop for tag in tags + do + (ignore-errors (org-gnosis--insert-into 'tags `([,tag]))) + (org-gnosis--insert-into 'node-tag `([,id ,tag]))) + ;; Insert master relationship as link + (when (and master (stringp master)) + (org-gnosis--insert-into 'links `([,id ,master]))))) + ;; Insert ID links + (cl-loop for link in links + do (org-gnosis--insert-into 'links `[,(cdr link) ,(car link)])))) + (error (message "Error updating %s: %s" file err)))) (defun org-gnosis--delete-file (&optional file) "Delete contents for FILE in database." @@ -527,11 +567,16 @@ If JOURNAL-P is non-nil, retrieve/create node as a journal entry." "Visit backlinks for current node." (interactive) (let* ((id (org-gnosis-get-id)) - (links (org-gnosis-select 'source 'links `(= dest ,id) t)) - (titles (cl-loop for link in links - collect (org-gnosis-select 'title 'nodes `(= id ,link) t)))) - (org-gnosis-find - (completing-read "Backlink: " (apply #'append titles))))) + (backlinks (org-gnosis-select '[source] 'links `(= dest ,id))) + (titles (cl-loop for backlink in backlinks + for source-id = (car backlink) + for title = (caar (org-gnosis-select 'title 'nodes + `(= id ,source-id))) + when title collect title))) + (if titles + (org-gnosis-find + (completing-read "Backlink: " titles)) + (message "No backlinks found for current node")))) ;;;###autoload (defun org-gnosis-journal-find (&optional title)