From ed4f18066c9d6746b7ae0b1ef6b39bebff531569 Mon Sep 17 00:00:00 2001 From: Folkert van der Beek Date: Tue, 2 Jul 2019 22:46:39 +0200 Subject: [PATCH] Convert internal links for subtree exports Fixes https://github.com/kaushalmodi/ox-hugo/issues/30. - Locate correct subtree to be exported - Made CUSTOM_ID references unique - Updated .md files with unique CUSTOM_ID references - Workaround to prevent exporting of empty special blocks - Mark preprocessed buffer as unmodified - Support crosspost custom-id, id and fuzzy link types - Update test site with cross-post links - Require 'org-id in setup-ox-hugo.el to handle ID links - Add instruction to handle ID links to test site - Kill the preprocessed buffer after exporting - Fix check for non-nil destination-filename - Support preprocessing of cross-post "fuzzy" and "id" type links - Support section paths in the preprocessing buffer - Retrieve #+HUGO_SECTION keyword from in-buffer settings --- ox-hugo.el | 136 ++++++++++++++++-- test/setup-ox-hugo.el | 1 + test/site/content-org/all-posts.org | 67 ++++++++- .../content/posts/citations-example-toml.md | 4 +- .../content/posts/citations-example-yaml.md | 4 +- test/site/content/posts/link-destination.md | 9 ++ .../posts/links-outside-the-same-post.md | 65 +++++++++ 7 files changed, 270 insertions(+), 16 deletions(-) create mode 100644 test/site/content/posts/link-destination.md create mode 100644 test/site/content/posts/links-outside-the-same-post.md diff --git a/ox-hugo.el b/ox-hugo.el index a4757c5..7a3d44e 100644 --- a/ox-hugo.el +++ b/ox-hugo.el @@ -3790,6 +3790,117 @@ exporting all valid Hugo post subtrees from the current Org file. (message "Point is not in a valid Hugo post subtree; move to one and try again")) valid-subtree-found)))) +(defun org-hugo--get-element-path (element info) + "Return the section path of ELEMENT. +INFO is a plist holding export options." + (let ((root (or (org-export-get-node-property :EXPORT_HUGO_SECTION element t) + (plist-get info :hugo-section))) + (filename (org-export-get-node-property :EXPORT_FILE_NAME element t)) + current-element + fragment + fragments) + (setq current-element element) + ;; Iterate over all parents of current-element, and collect section path fragments + (while (and current-element (not (org-export-get-node-property :EXPORT_HUGO_SECTION current-element nil))) + ;; Add the :EXPORT_HUGO_SECTION* value to the fragment list + (when (setq fragment (org-export-get-node-property :EXPORT_HUGO_SECTION* current-element nil)) + (push fragment fragments)) + (setq current-element (org-element-property :parent current-element))) + ;; Return the root section, section fragments and filename concatenated + (concat + (file-name-as-directory root) + (mapconcat #'file-name-as-directory fragments "") + filename))) + +(defun org-hugo--preprocess-buffer () + "Return a preprocessed copy of the current buffer. +Internal links to other subtrees are converted to external +links." + (let* ((buffer (generate-new-buffer (buffer-name))) + ;; Create an abstract syntax tree (AST) of the org document in the current buffer + (ast (org-element-parse-buffer)) + (org-use-property-inheritance (org-hugo--selective-property-inheritance)) + (info (org-combine-plists + (list :parse-tree ast) + (org-export--get-export-attributes 'hugo) + (org-export--get-buffer-attributes) + (org-export-get-environment 'hugo))) + (local-variables (buffer-local-variables)) + (bound-variables (org-export--list-bound-variables)) + vars) + (with-current-buffer buffer + (let ((inhibit-modification-hooks t) + (org-mode-hook nil) + (org-inhibit-startup t)) + (org-mode) + ;; Copy specific buffer local variables and variables set + ;; through BIND keywords. + (dolist (entry local-variables vars) + (when (consp entry) + (let ((var (car entry)) + (val (cdr entry))) + (and (not (memq var org-export-ignored-local-variables)) + (or (memq var + '(default-directory + buffer-file-name + buffer-file-coding-system)) + (assq var bound-variables) + (string-match "^\\(org-\\|orgtbl-\\)" + (symbol-name var))) + ;; Skip unreadable values, as they cannot be + ;; sent to external process. + (or (not val) (ignore-errors (read (format "%S" val)))) + (push (set (make-local-variable var) val) vars))))) + ;; Process all link elements in the AST + (org-element-map ast 'link + (lambda (link) + (let ((type (org-element-property :type link))) + (when (member type '("custom-id" "id" "fuzzy")) + (let* ((raw-link (org-element-property :raw-link link)) + (destination (if (string= type "fuzzy") + (org-export-resolve-fuzzy-link link info) + (org-export-resolve-id-link link info))) + (source-path (org-hugo--get-element-path link info)) + (destination-path (org-hugo--get-element-path destination info))) + ;; Change the link if it points to a valid destination outside the subtree + (unless (equal source-path destination-path) + (let ((link-copy (org-element-copy link))) + (apply #'org-element-adopt-elements link-copy (org-element-contents link)) + (org-element-put-property link-copy :type "file") + (org-element-put-property link-copy :path + (cond + ;; If the destination is a heading with the :EXPORT_FILE_NAME + ;; property defined, the link should point to the file (without + ;; anchor) + ((org-element-property :EXPORT_FILE_NAME destination) + (concat destination-path ".org")) + ;; Hugo only supports anchors to headlines, so if a "fuzzy" type + ;; link points to anything else than a headline, it should point + ;; to the file. + ((and (string= type "fuzzy") (not (string-prefix-p "*" raw-link))) + (concat destination-path ".org")) + ;; In "custom-id" type links, the raw-link matches the anchor of + ;; the destination. + ((string= type "custom-id") + (concat destination-path ".org::" raw-link)) + ;; In "id" and "fuzzy" type links, the anchor of the destination + ;; is derived from the :CUSTOM_ID property or the title + (t + (let* ((custom-id (org-element-property :CUSTOM_ID destination)) + (title (org-element-property :raw-value destination)) + (anchor (or custom-id (org-hugo-slug title)))) + (concat destination-path ".org::#" anchor))))) + (org-element-set-element link link-copy)))))))) + ;; Workaround to prevent exporting of empty special blocks + (org-element-map ast 'special-block + (lambda (block) + (when (null (org-element-contents block)) + (org-element-adopt-elements block "")))) + ;; Turn the AST with updated links into an org document + (insert (org-element-interpret-data ast)) + (set-buffer-modified-p nil))) + buffer)) + ;;; Interactive functions @@ -3923,13 +4034,17 @@ The optional argument NOERROR is passed to (if all-subtrees (progn (setq org-hugo--subtree-count 0) ;Reset the subtree count - (setq ret (org-map-entries - (lambda () - (org-hugo--export-subtree-to-md async visible-only :print-subtree-count)) - ;; Export only the subtrees where - ;; EXPORT_FILE_NAME property is not - ;; empty. - "EXPORT_FILE_NAME<>\"\"")) + (let ((buffer (org-hugo--preprocess-buffer))) + (with-current-buffer buffer + (setq ret (org-map-entries + (lambda () + (org-hugo--export-subtree-to-md async visible-only + :print-subtree-count)) + ;; Export only the subtrees where + ;; EXPORT_FILE_NAME property is not + ;; empty. + "EXPORT_FILE_NAME<>\"\"")) + (kill-buffer buffer))) (if ret (message "[ox-hugo] Exported %d subtree%s from %s" org-hugo--subtree-count @@ -3942,7 +4057,12 @@ The optional argument NOERROR is passed to (setq ret (org-hugo--export-file-to-md f-or-b-name async visible-only noerror)))) ;; Publish only the current subtree. - (setq ret (org-hugo--export-subtree-to-md async visible-only)) + (let ((buffer (org-hugo--preprocess-buffer)) + (outline-path (org-get-outline-path t))) + (with-current-buffer buffer + (goto-char (org-find-olp outline-path t)) + (setq ret (org-hugo--export-subtree-to-md async visible-only))) + (kill-buffer buffer)) (unless ret (setq ret (org-hugo--export-file-to-md f-or-b-name async visible-only noerror))))))) ret)) diff --git a/test/setup-ox-hugo.el b/test/setup-ox-hugo.el index a2b515d..d1e449c 100644 --- a/test/setup-ox-hugo.el +++ b/test/setup-ox-hugo.el @@ -197,6 +197,7 @@ Emacs installation. If Emacs is installed using ;; (message (list-load-path-shadows :stringp)) ) +(require 'org-id) (require 'ox-hugo) (defun org-hugo-export-all-wim-to-md () (org-hugo-export-wim-to-md :all-subtrees nil nil :noerror)) diff --git a/test/site/content-org/all-posts.org b/test/site/content-org/all-posts.org index 8303e0b..643543b 100644 --- a/test/site/content-org/all-posts.org +++ b/test/site/content-org/all-posts.org @@ -3020,6 +3020,65 @@ C = W\log_{2} (1+\mathrm{SNR}) \end{equation} *Here we refer to equation \ref{eq:1}.* +** Links outside the same post +:PROPERTIES: +:EXPORT_FILE_NAME: links-outside-the-same-post +:END: +{{{oxhugoissue(30)}}} +*** External links with search options :external_links: +Links between documents can contain some search options. Only links +to a heading with a *:CUSTOM_ID* property will be resolved to the +appropriate location in the linked file. Links to headlines and +links to targets will be resolved to the containing file. + +[[file:link-destination.org::#external-target][Link to CUSTOM_ID]] +[[file:link-destination.org::*external-target][Link to a headline]] +[[file:link-destination.org::external-target][Link to a target]] +*** Internal links :internal_links: +Internal links point to targets in the current subtree that will be +exported to the same Hugo post as the link source. To handle links to +an *:ID* property, the =org-id= feature must first be loaded, either +through =org-customize= or by adding =(require 'org-id)= in your Emacs +init file. + +[[#internal-target][Link to CUSTOM_ID within the same post]] +[[id:8e65ff86-3f9a-48ef-9b43-751a2e8a9372][Link to ID within the same post]] +[[*Internal target][Link to headline within the same post]] +[[internal-target][Link to target within the same post]] +*** Cross-post links :crosspost_links: +Cross-post links are internal links pointing to targets in a different +subtree that will be exported to another Hugo post than the link +source in subtree-based exports. The Hugo's `ref` and `relref` +shortcodes only supports anchors to headlines, so links to a heading, +a *:CUSTOM_ID* property, or an *:ID* property will be resolved to the +appropriate location in the linked file, but links to targets will be +resolved to the containing post. + +[[#external-target][Link to CUSTOM_ID outside the same post]] +[[id:de0df718-f9b4-4449-bb0a-eb4402fa5fcb][Link to ID outside the same post]] +[[*External target][Link to headline outside current post]] +[[external-target][Link to target outside the same post]] +[[#link-destination][Link to subtree by CUSTOM_ID]] +[[id:1e5e0bcd-caea-40ad-a75b-e488634c2678][Link to subtree by ID]] +[[*Link destination][Link to subtree by headline]] +*** Internal target +:PROPERTIES: +:CUSTOM_ID: internal-target +:ID: 8e65ff86-3f9a-48ef-9b43-751a2e8a9372 +:END: +<> +*** Link destination +:PROPERTIES: +:CUSTOM_ID: link-destination +:EXPORT_FILE_NAME: link-destination +:ID: 1e5e0bcd-caea-40ad-a75b-e488634c2678 +:END: +**** External target +:PROPERTIES: +:CUSTOM_ID: external-target +:ID: de0df718-f9b4-4449-bb0a-eb4402fa5fcb +:END: +<> * Equations :equations:mathjax: ** Inline equations :PROPERTIES: @@ -4860,10 +4919,10 @@ Here is a test example file with an in-text citation where someone important says something important (e.g. @loncar2016). And here is another bit of blah with a footnote citation.[fn:5] -See [[#citation-example-section-2]]. +See [[#citation-example-toml-section-2]]. *** Section 2 :PROPERTIES: -:CUSTOM_ID: citation-example-section-2 +:CUSTOM_ID: citation-example-toml-section-2 :END: Content in section 2. *** Testing random Hugo shortcode @@ -4927,10 +4986,10 @@ Here is a test example file with an in-text citation where someone important says something important (e.g. @loncar2016). And here is another bit of blah with a footnote citation.[fn:5] -See [[#citation-example-section-2]]. +See [[#citation-example-yaml-section-2]]. *** Section 2 :PROPERTIES: -:CUSTOM_ID: citation-example-section-2 +:CUSTOM_ID: citation-example-yaml-section-2 :END: Content in section 2. ** Citation Linking :link_citations: diff --git a/test/site/content/posts/citations-example-toml.md b/test/site/content/posts/citations-example-toml.md index 936f6f8..f0ba099 100644 --- a/test/site/content/posts/citations-example-toml.md +++ b/test/site/content/posts/citations-example-toml.md @@ -17,9 +17,9 @@ Here is a test example file with an in-text citation where someone important says something important (e.g. Loncar (2016)). And here is another bit of blah with a footnote citation.[^fn:1] -See [Section 2](#citation-example-section-2). +See [Section 2](#citation-example-toml-section-2). -## Section 2 {#citation-example-section-2} +## Section 2 {#citation-example-toml-section-2} Content in section 2. diff --git a/test/site/content/posts/citations-example-yaml.md b/test/site/content/posts/citations-example-yaml.md index 9439716..b666e2f 100644 --- a/test/site/content/posts/citations-example-yaml.md +++ b/test/site/content/posts/citations-example-yaml.md @@ -16,9 +16,9 @@ Here is a test example file with an in-text citation where someone important says something important (e.g. Loncar (2016)). And here is another bit of blah with a footnote citation.[^fn:1] -See [Section 2](#citation-example-section-2). +See [Section 2](#citation-example-yaml-section-2). -## Section 2 {#citation-example-section-2} +## Section 2 {#citation-example-yaml-section-2} Content in section 2. diff --git a/test/site/content/posts/link-destination.md b/test/site/content/posts/link-destination.md new file mode 100644 index 0000000..f5d1d56 --- /dev/null +++ b/test/site/content/posts/link-destination.md @@ -0,0 +1,9 @@ ++++ +title = "Link destination" +tags = ["links"] +draft = false ++++ + +## External target {#external-target} + + diff --git a/test/site/content/posts/links-outside-the-same-post.md b/test/site/content/posts/links-outside-the-same-post.md new file mode 100644 index 0000000..514d2e2 --- /dev/null +++ b/test/site/content/posts/links-outside-the-same-post.md @@ -0,0 +1,65 @@ ++++ +title = "Links outside the same post" +tags = ["links"] +draft = false ++++ + +`ox-hugo` Issue #[30](https://github.com/kaushalmodi/ox-hugo/issues/30) + + +## External links with search options {#external-links-with-search-options} + +Links between documents can contain some search options. Only links +to a heading with a **:CUSTOM\_ID** property will be resolved to the +appropriate location in the linked file. Links to headlines and +links to targets will be resolved to the containing file. + +[Link to CUSTOM\_ID]({{< relref "link-destination#external-target" >}}) +[Link to a headline]({{< relref "link-destination" >}}) +[Link to a target]({{< relref "link-destination" >}}) + + +## Internal links {#internal-links} + +Internal links point to targets in the current subtree that will be +exported to the same Hugo post as the link source. To handle links to +an **:ID** property, the `org-id` feature must first be loaded, either +through `org-customize` or by adding `(require 'org-id)` in your Emacs +init file. + +[Link to CUSTOM\_ID within the same post](#internal-target) +[Link to ID within the same post](#internal-target) +[Link to headline within the same post](#internal-target) +[Link to target within the same post](#org2549435) + + +## Cross-post links {#cross-post-links} + +Cross-post links are internal links pointing to targets in a different +subtree that will be exported to another Hugo post than the link +source in subtree-based exports. The Hugo's \`ref\` and \`relref\` +shortcodes only supports anchors to headlines, so links to a heading, +a **:CUSTOM\_ID** property, or an **:ID** property will be resolved to the +appropriate location in the linked file, but links to targets will be +resolved to the containing post. + +[Link to CUSTOM\_ID outside the same post]({{< relref "link-destination#external-target" >}}) +[Link to ID outside the same post]({{< relref "link-destination#external-target" >}}) +[Link to headline outside current post]({{< relref "link-destination#external-target" >}}) +[Link to target outside the same post]({{< relref "link-destination" >}}) +[Link to subtree by CUSTOM\_ID]({{< relref "link-destination" >}}) +[Link to subtree by ID]({{< relref "link-destination" >}}) +[Link to subtree by headline]({{< relref "link-destination" >}}) + + +## Internal target {#internal-target} + + + + +## Link destination {#link-destination} + + +### External target {#external-target} + +