Implement numbered cross-references

* lisp/org.el (org-link-search): Search for #+name affiliated keywords
  and invisible targets.
* contrib/lisp/org-element.el (org-element-link-parser): Remove "ref"
  links relative part.
(org-element-target-parser): Move property name from `:raw-value' to
`:value'.
(org-element-recursive-objects): Remove targets from tables.  Cells
are not parsed unless explicitely asked by back-end developer, too
late.  A target wouldn't be noticed in time.  One solution could be to
parse every table, but that's time consumming.
(org-element-object-restrictions): Target are not recursive anymore.
* contrib/lisp/org-export.el (org-export-resolve-fuzzy-link): Find
  elements with a matching "#+name: path" affiliated keyword.
(org-export-get-ordinal): Make special cases for headlines, items,
footnotes definitions and references.
(org-export-resolve-ref-link): Removed function.
* EXPERIMENTAL/org-e-latex.el (org-e-latex-link): Handle
  cross-reference numbers.
(org-e-latex-target): Targets have no contents.
* EXPERIMENTAL/org-e-ascii.el (org-e-ascii--describe-links): Ignore
  fuzzy links in link description at the end of the section.
(org-e-ascii-link): Handle cross-reference numbers.
* testing/contrib/lisp/test-org-export.el: Add tests.
* testing/lisp/test-org.el: Add tests.
This commit is contained in:
Nicolas Goaziou 2012-02-20 22:24:38 +01:00
parent 7c8e9125cb
commit 933c0fa441
7 changed files with 297 additions and 140 deletions

View File

@ -820,28 +820,22 @@ channel."
(if (not desc) (org-element-property :raw-link link)
(org-export-secondary-string desc 'e-ascii info)))))
(cond
;; Coderefs, radio links and ref links are ignored.
((member type '("coderef" "radio" "ref")) nil)
;; Id, custom-id and fuzzy links (with the exception of
;; targets): Headlines refer to their numbering.
((member type '("custom-id" "fuzzy" "id"))
(let ((destination (if (string= type "fuzzy")
(org-export-resolve-fuzzy-link link info)
(org-export-resolve-id-link link info))))
(unless (eq (org-element-type destination) 'target)
(concat
(org-e-ascii--fill-string
(format
"[%s] %s"
anchor
(if (not destination)
(org-e-ascii--translate "Unknown reference" info)
(format
(org-e-ascii--translate "See section %s" info)
(mapconcat 'number-to-string
(org-export-get-headline-number destination info)
"."))))
width info) "\n\n"))))
;; Coderefs, radio links and fuzzy links are ignored.
((member type '("coderef" "radio" "fuzzy")) nil)
;; Id and custom-id links: Headlines refer to their numbering.
((member type '("custom-id" "id"))
(let ((dest (org-export-resolve-id-link link info)))
(concat
(org-e-ascii--fill-string
(format
"[%s] %s"
anchor
(if (not dest) (org-e-ascii--translate "Unknown reference" info)
(format
(org-e-ascii--translate "See section %s" info)
(mapconcat 'number-to-string
(org-export-get-headline-number dest info) "."))))
width info) "\n\n")))
;; Do not add a link that cannot be resolved and doesn't have
;; any description: destination is already visible in the
;; paragraph.
@ -1385,29 +1379,23 @@ INFO is a plist holding contextual information."
(org-element-property :path link)
(cdr (assq 'radio-target org-element-object-restrictions)))
'e-ascii info))
;; Ref link: If there's no description (DESC, return link's
;; destination sequence number among elements of same
;; type. Otherwise, use DESC.
((string= type "ref")
(if (org-string-nw-p desc) desc
(format "%d"
(org-export-get-ordinal
(org-export-resolve-ref-link link info)
info nil nil
(lambda (el) (or (org-element-property :caption el)
(org-element-property :name el)))))))
;; Do not apply a special syntax on fuzzy links pointing to
;; targets.
((and (string= type "fuzzy")
(let ((path (org-element-property :path link)))
(loop for target in (plist-get info :target-list)
thereis (string=
(org-element-property :raw-value target)
path))))
(if (org-string-nw-p desc) desc raw-link))
((string= type "fuzzy")
(let ((destination (org-export-resolve-fuzzy-link link info)))
;; Ignore invisible "#+target: path".
(unless (eq (org-element-type destination) 'keyword)
(if (org-string-nw-p desc) desc
(when destination
(let ((number (org-export-get-ordinal destination info)))
(when number
(if (atom number) (number-to-string number)
(mapconcat 'number-to-string number ".")))))))))
(t
(concat (format "[%s]" (if (org-string-nw-p desc) desc raw-link))
(unless org-e-ascii-links-to-notes (format " (%s)" raw-link)))))))
(if (not (org-string-nw-p desc)) (format "[%s]" raw-link)
(concat
(format "[%s]" desc)
(unless org-e-ascii-links-to-notes (format " (%s)" raw-link))))))))
;;;; Macro
@ -1845,11 +1833,7 @@ INFO is a plist used as a communication channel."
;;;; Target
(defun org-e-ascii-target (target contents info)
"Transcode a TARGET object from Org to ASCII.
CONTENTS is the contents of the target. INFO is a plist holding
contextual information."
contents)
;; Targets are invisible.
;;;; Time-stamp

View File

@ -1289,8 +1289,8 @@ CONTENTS is nil. INFO is a plist holding contextual information."
(cond
((string= key "latex") value)
((string= key "index") (format "\\index{%s}" value))
((string= key "target")
(format "\\label{%s}" (org-export-solidify-link-text value)))
;; Invisible targets.
((string= key "target") nil)
((string= key "toc")
(let ((value (downcase value)))
(cond
@ -1449,32 +1449,26 @@ INFO is a plist holding contextual information. See
(org-element-parse-secondary-string
path (cdr (assq 'radio-target org-element-object-restrictions)))
'e-latex info)))
;; Ref link: If no description is provided, reference label PATH
;; and display table number. Otherwise move to label but display
;; description instead.
((string= type "ref")
(if (not desc) (format "\\ref{%s}" path)
(format "\\hyperref[%s]{%s}" path desc)))
;; Links pointing to an headline: Find destination and build
;; appropriate referencing command.
((member type '("custom-id" "fuzzy" "id"))
(let ((destination (if (string= type "fuzzy")
(org-export-resolve-fuzzy-link link info)
(org-export-resolve-id-link link info))))
;; Fuzzy link points to a target. Do as above.
(case (org-element-type destination)
(target
(format "\\hyperref[%s]{%s}"
(org-export-solidify-link-text
(org-element-property :raw-value destination))
;; Fuzzy link points nowhere.
('nil
(format "\\texttt{%s}"
(or desc
(org-export-secondary-string
(org-element-property :raw-link link)
'e-latex info))))
;; Fuzzy link points to an headline. If headlines are
;; numbered and the link has no description, display
;; headline's number. Otherwise, display description or
;; headline's title.
;; Fuzzy link points to an invisible target.
(keyword nil)
;; LINK points to an headline. If headlines are numbered
;; and the link has no description, display headline's
;; number. Otherwise, display description or headline's
;; title.
(headline
(let ((label
(format "sec-%s"
@ -1489,13 +1483,11 @@ INFO is a plist holding contextual information. See
(org-export-secondary-string
(org-element-property :title destination)
'e-latex info))))))
;; Fuzzy link points nowhere.
;; Fuzzy link points to a target. Do as above.
(otherwise
(format "\\texttt{%s}"
(or desc
(org-export-secondary-string
(org-element-property :raw-link link)
'e-latex info)))))))
(let ((path (org-export-solidify-link-text path)))
(if (not desc) (format "\\ref{%s}" path)
(format "\\hyperref[%s]{%s}" path desc)))))))
;; Coderef: replace link with the reference name or the
;; equivalent line number.
((string= type "coderef")
@ -1968,14 +1960,12 @@ CONTENTS is nil. INFO is a plist holding contextual information."
;;;; Target
(defun org-e-latex-target (target text info)
(defun org-e-latex-target (target contents info)
"Transcode a TARGET object from Org to LaTeX.
TEXT is the text of the target. INFO is a plist holding
contextual information."
(format "\\label{%s}%s"
(org-export-solidify-link-text
(org-element-property :raw-value target))
text))
CONTENTS is nil. INFO is a plist holding contextual
information."
(format "\\label{%s}"
(org-export-solidify-link-text (org-element-property :value target))))
;;;; Time-stamp

View File

@ -1967,9 +1967,6 @@ Assume point is at the beginning of the link."
;; Explicit type (http, irc, bbdb...). See `org-link-types'.
((string-match org-link-re-with-space3 link)
(setq type (match-string 1 link) path (match-string 2 link)))
;; Ref type: PATH is the name of the target element.
((string-match "^ref:\\(.*\\)" link)
(setq type "ref" path (org-trim (match-string 1 link))))
;; Id type: PATH is the id.
((string-match "^id:\\([-a-f0-9]+\\)" link)
(setq type "id" path (match-string 1 link)))
@ -2269,25 +2266,21 @@ CONTENTS is the contents of the object."
"Parse target at point.
Return a list whose car is `target' and cdr a plist with
`:begin', `:end', `:contents-begin', `:contents-end', `raw-value'
and `:post-blank' as keywords.
`:begin', `:end', `:contents-begin', `:contents-end', `value' and
`:post-blank' as keywords.
Assume point is at the target."
(save-excursion
(looking-at org-target-regexp)
(let ((begin (point))
(contents-begin (match-beginning 1))
(contents-end (match-end 1))
(raw-value (org-match-string-no-properties 1))
(value (org-match-string-no-properties 1))
(post-blank (progn (goto-char (match-end 0))
(skip-chars-forward " \t")))
(end (point)))
`(target
(:begin ,begin
:end ,end
:contents-begin ,contents-begin
:contents-end ,contents-end
:raw-value ,raw-value
:value ,value
:post-blank ,post-blank)))))
(defun org-element-target-interpreter (target contents)
@ -2481,7 +2474,7 @@ regexp matching one object can also match the other object.")
"Complete list of object types.")
(defconst org-element-recursive-objects
'(emphasis link macro subscript superscript target radio-target)
'(emphasis link macro subscript superscript radio-target)
"List of recursive object types.")
(defconst org-element-non-recursive-block-alist
@ -2551,8 +2544,7 @@ This list is checked after translations have been applied. See
(subscript entity export-snippet inline-babel-call inline-src-block
latex-fragment sub/superscript text-markup)
(superscript entity export-snippet inline-babel-call inline-src-block
latex-fragment sub/superscript text-markup)
(target entity export-snippet latex-fragment sub/superscript text-markup))
latex-fragment sub/superscript text-markup))
"Alist of recursive objects restrictions.
CAR is a recursive object type and CDR is a list of successors

View File

@ -1289,7 +1289,13 @@ Following tree properties are set:
`(:parse-tree
,data
:target-list
,(org-element-map data 'target 'identity info)
,(org-element-map
data '(keyword target)
(lambda (blob)
(when (or (eq (org-element-type blob) 'target)
(string= (upcase (org-element-property :key blob))
"TARGET"))
blob)) info)
:headline-numbering ,(org-export-collect-headline-numbering data info)
:back-end ,backend)
info))
@ -2704,8 +2710,11 @@ INFO is a plist holding contextual information.
Return value can be an object, an element, or nil:
- If LINK path exactly matches any target, return the target
object.
- If LINK path matches a target object (i.e. <<path>>) or
element (i.e. \"#+target: path\"), return it.
- If LINK path exactly matches the name affiliated keyword
\(i.e. #+name: path) of an element, return that element.
- If LINK path exactly matches any headline name, return that
element. If more than one headline share that name, priority
@ -2716,16 +2725,29 @@ Return value can be an object, an element, or nil:
Assume LINK type is \"fuzzy\"."
(let ((path (org-element-property :path link)))
;; Link points to a target: return it.
(or (loop for target in (plist-get info :target-list)
when (string= (org-element-property :raw-value target) path)
return target)
;; Link either points to an headline or nothing. Try to find
;; the source, with priority given to headlines with the closest
;; common ancestor. If such candidate is found, return its
;; beginning position as an unique identifier, otherwise return
;; nil.
(let ((find-headline
(cond
;; First try to find a matching "<<path>>" unless user specified
;; he was looking for an headline (path starts with a *
;; character).
((and (not (eq (substring path 0 1) ?*))
(loop for target in (plist-get info :target-list)
when (string= (org-element-property :value target) path)
return target)))
;; Then try to find an element with a matching "#+name: path"
;; affiliated keyword.
((and (not (eq (substring path 0 1) ?*))
(org-element-map
(plist-get info :parse-tree) org-element-all-elements
(lambda (el)
(when (string= (org-element-property :name el) path) el))
info 'first-match)))
;; Last case: link either points to an headline or to
;; nothingness. Try to find the source, with priority given to
;; headlines with the closest common ancestor. If such candidate
;; is found, return its beginning position as an unique
;; identifier, otherwise return nil.
(t
(let ((find-headline
(function
;; Return first headline whose `:raw-value' property
;; is NAME in parse tree DATA, or nil.
@ -2748,7 +2770,7 @@ Assume LINK type is \"fuzzy\"."
(when foundp (throw 'exit foundp)))))
(org-export-get-genealogy link info)) nil)
;; No match with a common ancestor: try the full parse-tree.
(funcall find-headline path (plist-get info :parse-tree)))))))
(funcall find-headline path (plist-get info :parse-tree))))))))
(defun org-export-resolve-id-link (link info)
"Return headline referenced as LINK destination.
@ -2766,20 +2788,6 @@ is either \"id\" or \"custom-id\"."
headline))
info 'first-match)))
(defun org-export-resolve-ref-link (link info)
"Return element referenced as LINK destination.
INFO is a plist used as a communication channel.
Assume LINK type is \"ref\" and. Return value is the first
element whose `:name' property matches LINK's `:path', or nil."
(let ((name (org-element-property :path link)))
(org-element-map
(plist-get info :parse-tree) org-element-all-elements
(lambda (el)
(when (string= (org-element-property :name el) name) el))
info 'first-match)))
(defun org-export-resolve-coderef (ref info)
"Resolve a code reference REF.
@ -2870,27 +2878,62 @@ Optional argument PREDICATE is a function returning a non-nil
value if the current element or object should be counted in. It
accepts one argument: the element or object being considered.
This argument allows to count only a certain type of objects,
like inline images, which are a subset of links \(in that case,
`org-export-inline-image-p' might be an useful predicate\)."
(let ((counter 0)
;; Determine if search should apply to current section, in
;; which case it should be retrieved first, or to full parse
;; tree. As a special case, an element or object without
;; a parent headline will also trigger a full search,
;; notwithstanding WITHIN-SECTION value.
(data
(if (not within-section) (plist-get info :parse-tree)
(or (org-export-get-parent-headline element info)
(plist-get info :parse-tree)))))
;; Increment counter until ELEMENT is found again.
(org-element-map
data (or types (org-element-type element))
(lambda (el)
(cond
((equal element el) (1+ counter))
((not predicate) (incf counter) nil)
((funcall predicate el) (incf counter) nil)))
info 'first-match)))
like inline images, which are a subset of links (in that case,
`org-export-inline-image-p' might be an useful predicate).
Return value is a list of numbers if ELEMENT is an headline or an
item. It is nil for keywords. It represents the footnote number
for footnote definitions and footnote references. If ELEMENT is
a target, return the same value as if ELEMENT was the closest
table, item or headline containing the target. In any other
case, return the sequence number of ELEMENT among elements or
objects of the same type."
;; A target keyword, representing an invisible target, never has
;; a sequence number.
(unless (eq (org-element-type element) 'keyword)
;; Ordinal of a target object refer to the ordinal of the closest
;; table, item, or headline containing the object.
(when (eq (org-element-type element) 'target)
(setq element
(loop for parent in (org-export-get-genealogy element info)
when
(memq
(org-element-type parent)
'(footnote-definition footnote-reference headline item
table))
return parent)))
(case (org-element-type element)
;; Special case 1: An headline returns its number as a list.
(headline (org-export-get-headline-number element info))
;; Special case 2: An item returns its number as a list.
(item (let ((struct (org-element-property :structure element)))
(org-list-get-item-number
(org-element-property :begin element)
struct
(org-list-prevs-alist struct)
(org-list-parents-alist struct))))
((footnote definition footnote-reference)
(org-export-get-footnote-number element info))
(otherwise
(let ((counter 0)
;; Determine if search should apply to current section,
;; in which case it should be retrieved first, or to full
;; parse tree. As a special case, an element or object
;; without a parent headline will also trigger a full
;; search, notwithstanding WITHIN-SECTION value.
(data
(if (not within-section) (plist-get info :parse-tree)
(or (org-export-get-parent-headline element info)
(plist-get info :parse-tree)))))
;; Increment counter until ELEMENT is found again.
(org-element-map
data (or types (org-element-type element))
(lambda (el)
(cond
((equal element el) (1+ counter))
((not predicate) (incf counter) nil)
((funcall predicate el) (incf counter) nil)))
info 'first-match))))))
;;;; For Src-Blocks

View File

@ -9898,6 +9898,22 @@ visibility around point, thus ignoring
pos (match-beginning 0))))
;; There is an exact target for this
(goto-char pos))
((save-excursion
(goto-char (point-min))
(and
(re-search-forward
(format "^[ \t]*#\\+TARGET: %s" (regexp-quote s0)) nil t)
(setq type 'dedicated pos (match-beginning 0))))
;; Found an invisible target.
(goto-char pos))
((save-excursion
(goto-char (point-min))
(and
(re-search-forward
(format "^[ \t]*#\\+NAME: %s" (regexp-quote s0)) nil t)
(setq type 'dedicated pos (match-beginning 0))))
;; Found an element with a matching #+name affiliated keyword.
(goto-char pos))
((and (string-match "^(\\(.*\\))$" s0)
(save-excursion
(goto-char (point-min))

View File

@ -364,3 +364,94 @@ body\n")))
;; Both footnotes should be seen.
(should
(= (length (org-export-collect-footnote-definitions tree info)) 2))))))
(ert-deftest test-org-export/fuzzy-links ()
"Test fuzz link export specifications."
;; 1. Links to invisible (keyword) targets should be ignored.
(org-test-with-temp-text
"Paragraph.\n#+TARGET: Test\n[[Test]]"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should-not
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info))))
;; 2. Link to an headline should return headline's number.
(org-test-with-temp-text
"Paragraph.\n* Head1\n* Head2\n* Head3\n[[Head2]]"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should
;; Note: Headline's number is in fact a list of numbers.
(equal '(2)
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info t)))))
;; 3. Link to a target in an item should return item's number.
(org-test-with-temp-text
"- Item1\n - Item11\n - <<test>>Item12\n- Item2\n\n\n[[test]]"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should
;; Note: Item's number is in fact a list of numbers.
(equal '(1 2)
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info t)))))
;; 4. Link to a target in a footnote should return footnote's
;; number.
(org-test-with-temp-text
"Paragraph[1][2][fn:lbl3:C<<target>>][[test]][[target]]\n[1] A\n\n[2] <<test>>B"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should
(equal '(2 3)
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info)))))
;; 5. Link to a named element should return sequence number of that
;; element.
(org-test-with-temp-text
"#+NAME: tbl1\n|1|2|\n#+NAME: tbl2\n|3|4|\n#+NAME: tbl3\n|5|6|\n[[tbl2]]"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should
(= 2
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info t)))))
;; 6. Link to a target not within an item, a table, a footnote
;; reference or definition should return section number.
(org-test-with-temp-text
"* Head1\n* Head2\nParagraph<<target>>\n* Head3\n[[target]]"
(let* ((tree (org-element-parse-buffer))
(info (org-combine-plists (org-export-initial-options))))
(setq info (org-combine-plists
info (org-export-collect-tree-properties tree info 'test)))
(should
(equal '(2)
(org-element-map
tree 'link
(lambda (link)
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info t))))))

View File

@ -88,6 +88,47 @@ http://article.gmane.org/gmane.emacs.orgmode/21459/"
(org-babel-next-src-block)
(should (equal '(2 1) (org-babel-execute-src-block)))))
;;; Links
;;;; Fuzzy links
;; Fuzzy links [[text]] encompass links to a target (<<text>>), to
;; a target keyword (aka an invisible target: #+TARGET: text), to
;; a named element (#+name: text) and to headlines (* Text).
(ert-deftest test-org-export/fuzzy-links ()
"Test fuzzy links specifications."
;; 1. Fuzzy link goes in priority to a matching target.
(org-test-with-temp-text
"#+TARGET: Test\n#+NAME: Test\n|a|b|\n<<Test>>\n* Test\n[[Test]]"
(goto-line 4)
(org-open-at-point)
(should (looking-at "<<Test>>")))
;; 2. Fuzzy link should then go to a matching target keyword.
(org-test-with-temp-text
"#+NAME: Test\n|a|b|\n#+TARGET: Test\n* Test\n[[Test]]"
(goto-line 4)
(org-open-at-point)
(should (looking-at "#\\+TARGET: Test")))
;; 3. Then fuzzy link points to an element with a given name.
(org-test-with-temp-text "Test\n#+NAME: Test\n|a|b|\n* Test\n[[Test]]"
(goto-line 5)
(org-open-at-point)
(should (looking-at "#\\+NAME: Test")))
;; 4. A target still lead to a matching headline otherwise.
(org-test-with-temp-text "* Head1\n* Head2\n*Head3\n[[Head2]]"
(goto-line 4)
(org-open-at-point)
(should (looking-at "\\* Head2")))
;; 5. With a leading star in link, enforce heading match.
(org-test-with-temp-text "#+TARGET: Test\n* Test\n<<Test>>\n[[*Test]]"
(goto-line 4)
(org-open-at-point)
(should (looking-at "\\* Test"))))
(provide 'test-org)
;;; test-org.el ends here