branch: elpa/racket-mode
commit d9d78bc965c814c9c1a3d13fe5fbaa18c944ae48
Author: Greg Hendershott <[email protected]>
Commit: Greg Hendershott <[email protected]>

    Support documentation language families
    
    Implement latest concepts introduced in Racket 9.1.
    
    As discussed in <https://github.com/racket/racket/pull/5409>.
    
    - racket-hash-lang: Expose get-info documentation-language-family.
    
    - Expose get-main-language and get-language-families in new
    doc-families back end command. Return a cons of the main family name,
    and, a list of association lists with details for each family. The
    back end normalizes some variations in data, as well as changing
    abstract module paths into concrete absolute real paths.
    
    - Extract language family from documentation page. Display in header
    line. Supply racket-describe-choose-language-family to pick a family
    for "navigating as", akin to web browser, which ca affect "top" and
    "up" navigation, as well as sorting search candidates. The command is
    bound to a key, as well as button-ized family text in the header line.
    
    - Implement racket-describe-about-language-family, to view the doc
    page (if any) describing the family (often a "Guide" top page).
    
    - describe-search: Sort by the one "preferred" family first, and then
    sort by other families that an index item may supply, and using the
    'order key from get-language-families. This means for example that
    search candidates for HtDP will appear /after/ those for Rhombus, when
    the preferred family is neither.
    
    - Fix bug extracting prev/up/next links. (Before, when no prev link,
    we'd mismatch up and next.) Strictly speaking this is independent of
    doc lang families, but I didn't preserve it as a distinct commit.
---
 doc/racket-mode.texi        |   4 +
 racket-describe.el          | 263 ++++++++++++++++++++++++++++++++++----------
 racket-hash-lang.el         |  11 +-
 racket-scribble.el          |  33 +++---
 racket/command-server.rkt   |   3 +-
 racket/hash-lang-bridge.rkt |   4 +-
 racket/hash-lang.rkt        |   4 +-
 racket/lang-info.rkt        |   3 +-
 racket/scribble.rkt         |  80 ++++++++++++--
 9 files changed, 318 insertions(+), 87 deletions(-)

diff --git a/doc/racket-mode.texi b/doc/racket-mode.texi
index a269ed79ceb..ee99027aeb9 100644
--- a/doc/racket-mode.texi
+++ b/doc/racket-mode.texi
@@ -2204,6 +2204,10 @@ browser program -- are given @ref{racket-ext-link-face}.
 @tab @code{racket-describe-nav-top}
 @item @kbd{x} 
 @tab @code{racket-describe-browse-external}
+@item @kbd{L} 
+@tab @code{racket-describe-choose-language-family}
+@item @kbd{a} 
+@tab @code{racket-describe-about-language-family}
 @end multitable
 
 
diff --git a/racket-describe.el b/racket-describe.el
index 026838c0257..5c6b61efc23 100644
--- a/racket-describe.el
+++ b/racket-describe.el
@@ -1,6 +1,6 @@
 ;;; racket-describe.el -*- lexical-binding: t -*-
 
-;; Copyright (c) 2013-2025 by Greg Hendershott.
+;; Copyright (c) 2013-2026 by Greg Hendershott.
 ;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc.
 
 ;; Author: Greg Hendershott
@@ -19,7 +19,10 @@
 (require 'racket-back-end)
 ;; Don't (require 'racket-repl). Mutual dependency. Instead:
 (declare-function racket--repl-session-id "racket-repl" ())
-(autoload         'racket--repl-session-id "racket-repl")
+(autoload        'racket--repl-session-id "racket-repl")
+;; Don't (require 'racket-hash-lang-mode). Mutual dependency. Instead:
+(declare-function racket--hash-lang-doc-family "racket-hash-lang" ())
+(autoload        'racket--hash-lang-doc-family "racket-hash-lang")
 
 (defvar-local racket--describe-here nil
   "The current navigation point. Either nil or (cons path point).")
@@ -28,6 +31,31 @@
 (defvar-local racket--describe-stack-forward nil
   "Forward navigation list. Each item is (cons path point).")
 
+(defvar-local racket--describe-page-family nil
+  "The language family extracted from documentation page.")
+
+(defvar-local racket--describe-as-family nil
+  "When not nil and not equal to `racket--describe-page-family', show
+different UX and use this family for things like top and search sort
+priority.")
+
+(defun racket--describe-family-default-and-choices ()
+  (racket--cmd/await nil `(doc-families)))
+(defun racket--describe-default-family ()
+  (or (car (racket--describe-family-default-and-choices))
+      "Racket"))
+(defun racket--describe-family-choices ()
+  (cdr (racket--describe-family-default-and-choices)))
+(defun racket--describe-family-details (family)
+  (seq-some (lambda (v)
+              (and (string-equal family (cdr (assq 'family v)))
+                   v))
+            (racket--describe-family-choices)))
+(defun racket--describe-family-start-doc (family)
+  (cdr (assq 'start-doc (racket--describe-family-details family))))
+(defun racket--describe-family-describe-doc (family)
+  (cdr (assq 'describe-doc (racket--describe-family-details family))))
+
 (defun racket--call-with-describe-buffer (thunk)
   "Call THUNK in a blank `racket-describe-mode-buffer' and return
 the buffer.
@@ -114,11 +142,6 @@ anchor. If numberp, move to that position."
   (setq racket--describe-here
         (if path (cons path goto) nil))
   (setq racket--describe-nav nil)
-  (setq header-line-format
-        (propertize
-         (concat path (cond ((stringp goto) (concat " " goto))
-                            ((numberp goto) (format " %s" goto))))
-         'face '(:height 0.75)))
   ;; Although `shr' carefully fills to fit window width, if user
   ;; resizes window or changes text scaling, we don't want it to wrap.
   (setq truncate-lines t)
@@ -141,9 +164,16 @@ anchor. If numberp, move to that position."
              (racket-doc-link   . ,#'racket-render-tag-racket-doc-link)
              (racket-ext-link   . ,#'racket-render-tag-racket-ext-link)
              (racket-anchor     . ,#'racket-render-tag-racket-anchor)
-             (racket-nav        . ,#'racket-render-tag-racket-nav))))
+             (racket-nav        . ,#'racket-render-tag-racket-nav)
+             (racket-family     . ,#'racket-render-tag-racket-family))))
       (shr-insert-document
        (racket--describe-handle-toc-nodes dom)))
+    (let ((path+goto (format " %s %s" path (or goto ""))))
+     (setq header-line-format
+           `((:eval (racket--describe-family-header))
+             (:propertize ,path+goto
+                          face (:height 0.75)
+                          help-echo ,path+goto))))
     ;; See doc string for `racket--scribble-temp-nbsp'.
     (goto-char (point-min))
     (while (re-search-forward (string racket--scribble-temp-nbsp) nil t)
@@ -159,11 +189,11 @@ anchor. If numberp, move to that position."
 
 If `numberp', move to that position.
 
-If `stringp' move to the position after the anchor that is not
-anchor. There could be multiple anchors before some non-anchor
-text. We want point left where `racket-search-describe' can use
+If `stringp' move to the first position after the anchor that is not
+another anchor. There could be multiple anchors before some non-anchor
+text. We want to leave point where `racket-search-describe' can use
 `thing-at-point' to find a symbol."
-  (set-window-point ;in case buffer window isnt' selected; #590
+  (set-window-point ;in case buffer window isn't selected; #590
    (get-buffer-window (current-buffer))
    (cond
     ((numberp goto)
@@ -343,30 +373,124 @@ for our custom shr handler."
   (setq racket--describe-nav dom))
 
 (defun racket--describe-nav (which)
-  (interactive)
-  (let ((path (dom-attr racket--describe-nav which)))
-    (unless path
-      (user-error "There is no %s page available" which))
-    (setq racket--describe-stack-forward nil)
-    (racket--describe-maybe-push-here 'back)
-    (racket--describe-fetch-and-show path nil)))
+  (cl-flet ((top ()
+              (racket--describe-family-start-doc
+               (or racket--describe-as-family
+                   racket--describe-page-family))))
+    (let* ((dom-path (dom-attr racket--describe-nav which))
+           (path (pcase which
+                   ;; When there's a special language family top page,
+                   ;; (a) use it to go top and (b) don't go up beyond.
+                   ('top (or (top) dom-path))
+                   ('up (and (not (equal (top) (car racket--describe-here)))
+                             dom-path))
+                   (_ dom-path))))
+      (unless path
+        (user-error "No \"%s\" page is available%s"
+                    which
+                    (if (memq which '(top up))
+                        (format " (using language family \"%s\")"
+                                (or racket--describe-as-family
+                                    racket--describe-page-family))
+                      "")))
+      (setq racket--describe-stack-forward nil)
+      (racket--describe-maybe-push-here 'back)
+      (racket--describe-fetch-and-show path nil))))
 
 (defun racket-describe-nav-top ()
+  "Navigate to the top page."
   (interactive)
   (racket--describe-nav 'top))
 
 (defun racket-describe-nav-up ()
+  "Navigate to the parent page."
   (interactive)
   (racket--describe-nav 'up))
 
 (defun racket-describe-nav-prev ()
+  "Navigate to the previous page."
   (interactive)
   (racket--describe-nav 'prev))
 
 (defun racket-describe-nav-next ()
+  "Navigate to the next page."
   (interactive)
   (racket--describe-nav 'next))
 
+(defun racket-render-tag-racket-family (dom)
+  (setq racket--describe-page-family (dom-attr dom 'data-fam)))
+
+(defun racket--describe-family-header ()
+  (if (not racket--describe-page-family)
+      ""
+    ;; `buttonize' not availabe in older Emacs, so...
+    (cl-flet ((button (str help-echo)
+                (propertize
+                 str
+                 'button t
+                 'follow-link t
+                 'category t
+                 'keymap button-map
+                 'action #'racket-describe-choose-language-family
+                 'face '(:underline t :height 0.75)
+                 'mouse-face 'highlight
+                 'help-echo help-echo)))
+      (if (or (not racket--describe-as-family)
+              (equal racket--describe-as-family
+                     racket--describe-page-family))
+          (button
+           racket--describe-page-family
+           "Documentation language family; click to navigate using another 
family")
+        (concat
+         (propertize (concat racket--describe-page-family " ")
+                     'face '(:height 0.75)
+                     'help-echo "Documentation language family")
+         (button
+          (concat "(as " racket--describe-as-family ")")
+          "Navigating as family; click to change"))))))
+
+(defun racket--describe-completing-read-family (prompt)
+  (let* ((vs (racket--describe-family-choices))
+         (vs (seq-sort (lambda (a b)
+                         (or (> (or (cdr (assq 'order a)) 0)
+                                (or (cdr (assq 'order b)) 0))
+                             (string< (cdr (assq 'family a))
+                                      (cdr (assq 'family b)))))
+                       vs))
+         (names (seq-map (lambda (v)
+                           (cdr (assq 'family v)))
+                         vs)))
+    (completing-read prompt
+                     names
+                     nil ;predicate
+                     t   ;require-match
+                     nil ;initial-input
+                     'racket-documentation-language-family)))
+
+(defun racket-describe-choose-language-family (&optional _button)
+  "Choose a documentation language family with which to navigate.
+
+The family can affect `racket-describe-nav-top' and order in which
+`racket-describe-search' sorts candidates."
+  (interactive)
+  (when-let (name (racket--describe-completing-read-family
+                   "Navigate documentation as language family: "))
+    (setq racket--describe-as-family name)))
+
+(defun racket-describe-about-language-family ()
+  "Show the documentation page about a language family.
+
+For some languages, this will be something like a \"Guide\" table of
+contents. For other languages, it might be the same as the top page,
+i.e. `racket-describe-nav-top'."
+  (interactive)
+  (when-let (name (racket--describe-completing-read-family
+                   "View documentation about language family: "))
+    (when-let (about (racket--describe-family-describe-doc name))
+      (setq racket--describe-stack-forward nil)
+      (racket--describe-maybe-push-here 'back)
+      (racket--describe-fetch-and-show about nil))))
+
 (defun racket--describe-fetch-and-show (path goto)
   "Insert shr dom for PATH and move point to GOTO.
 
@@ -547,7 +671,9 @@ current line and looking back."
                 ("p"                 ,#'racket-describe-nav-prev)
                 ("^"                 ,#'racket-describe-nav-up)
                 ("C-^"               ,#'racket-describe-nav-top)
-                ("x"                 ,#'racket-describe-browse-external)))))
+                ("x"                 ,#'racket-describe-browse-external)
+                ("L"                 ,#'racket-describe-choose-language-family)
+                ("a"                 
,#'racket-describe-about-language-family)))))
     (define-key map [XF86Back]    'racket-describe-back)
     (define-key map [XF86Forward] 'racket-describe-back)
     (set-keymap-parent map special-mode-map)
@@ -615,6 +741,15 @@ Return nil or \(term path anchor lib\)."
                                          [32 racket-describe-search-from-libs]
                                          [0  
racket-describe-search-lang-fams]]))
          (candidates nil)
+         (first-family (or racket--describe-as-family ;buffer-local
+                           racket--describe-page-family ;buffer-local
+                           (racket--hash-lang-doc-family)
+                           (racket--describe-default-family)))
+         (family-orders (seq-map (lambda (v)
+                                   (cons (cdr (assq 'family v))
+                                         (- (or (cdr (assq 'order v))
+                                                0))))
+                                 (racket--describe-family-choices)))
          (collection
           (lambda (string predicate action)
             (cond
@@ -627,8 +762,11 @@ Return nil or \(term path anchor lib\)."
              (t
               (when (eq action t)
                 (setq candidates
-                      (racket--describe-search-make-strings
-                       (racket--cmd/await nil `(doc-search ,string)))))
+                      (mapcar (apply-partially
+                               #'racket--describe-search-make-string
+                               first-family
+                               family-orders)
+                              (racket--cmd/await nil `(doc-search ,string)))))
               (funcall (cond
                         ((null action) #'try-completion)
                         ((eq action t) #'all-completions)
@@ -640,7 +778,7 @@ Return nil or \(term path anchor lib\)."
           (lambda (v)
             (apply racket-doc-index-predicate-function
                    (get-text-property 0 'racket-affix v))))
-         (prompt "Search Racket documentation: ")
+         (prompt "Search documentation: ")
          (require-match t)
          (initial-input (racket--thing-at-point 'symbol t))
          (history 'racket-identifier)
@@ -658,28 +796,47 @@ Return nil or \(term path anchor lib\)."
          (add-to-history 'racket-identifier term) ;just term
          (list term path anchor lib))))))
 
-(defun racket--describe-search-make-strings (items)
-  "Make a list of candidate strings from back end ITEMS.
+(defun racket--describe-search-make-string (first-family family-orders item)
+  "Make a candidate string from back end ITEM.
+
+FIRST-FAMILY is a language family that, if an index item says that's one
+of its families, it should sort before all other similar items.
+
+FAMILY-ORDERS is an alist from family name to the \"order\" mapping
+supplied by get-language-families, albeit negated. This is used to sort
+index items by their language families, when none are FIRST-FAMILY.
 
 Each string has text properties needed by our affixation and
-display-sort functions.
-
-However `completing-read' returns a string stripped of text
-properties. :( So we append the path and anchor, tab separated,
-as invisible text. Use `racket--describe-search-parse-result' to
-extract."
-  (mapcar
-   (pcase-lambda (`(,term ,sort ,what ,from ,fams ,pkg-sort
-                          ,path ,anchor))
-     (let* ((term (propertize term
-                              'racket-affix (list what from fams)
-                              'racket-sort (list (format "%09d" sort)
-                                                 (format "%09d" pkg-sort))))
-            (lib (substring from 0 (string-match (rx ?,) from)))
-            (data (concat "\t" path "\t" anchor "\t" lib))
-            (data (propertize data 'display "")))
-       (concat term data)))
-   items))
+display-sort functions, used by `completing-read'.
+
+But note that `completing-read' returns a string stripped of text
+properties. To return values besides the term, we append them tab
+separated, as invisible text. Use `racket--describe-search-parse-result'
+to extract."
+  (pcase-let*
+      ((`(,term ,sort-order ,what ,from ,fams ,pkg-sort ,path ,anchor) item)
+       (affix (list what from (string-join fams ", ")))
+       (sort (list term
+                   (if (member first-family fams)
+                       " "
+                     (string-join
+                      (seq-map (lambda (fam)
+                                 (format "%09d"
+                                         (or (cdr (assoc fam family-orders))
+                                             0)))
+                               fams)))
+                   (string-join fams ",")
+                   (format "%09d" pkg-sort)
+                   from
+                   what
+                   (format "%09d" sort-order)))
+       (term (propertize term
+                         'racket-affix affix
+                         'racket-sort sort))
+       (lib (substring from 0 (string-match (rx ?,) from)))
+       (data (concat "\t" path "\t" anchor "\t" lib))
+       (data (propertize data 'display "")))
+    (concat term data)))
 
 (defun racket--describe-search-parse-result (str)
   (when (string-match (rx bos
@@ -695,24 +852,8 @@ extract."
 (defun racket--describe-search-display-sort (strs)
   "A value for display-sort-function metadata."
   (cl-flet*
-      ((term (s)
-         (substring s 0 (string-match (rx ?\t) s)))
-       (adjust-fams (fams)
-         (pcase fams
-           ("Racket" " Racket")
-           (v v)))
-       (key (v)
-         (pcase-let
-             ((`(,what ,from ,fams)
-               (get-text-property 0 'racket-affix v))
-              (`(,sort ,pkg-sort)
-               (get-text-property 0 'racket-sort v)))
-           (list (term v)
-                 (adjust-fams fams)
-                 pkg-sort
-                 from
-                 what
-                 sort)))
+      ((key (v)
+         (get-text-property 0 'racket-sort v))
        (key< (as bs)
          (cl-loop for a in as
                   for b in bs
diff --git a/racket-hash-lang.el b/racket-hash-lang.el
index fb9e4ea3342..667d3171024 100644
--- a/racket-hash-lang.el
+++ b/racket-hash-lang.el
@@ -1,6 +1,6 @@
 ;;; racket-hash-lang.el -*- lexical-binding: t; -*-
 
-;; Copyright (c) 2020-2025 by Greg Hendershott.
+;; Copyright (c) 2020-2026 by Greg Hendershott.
 ;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc.
 
 ;; Author: Greg Hendershott
@@ -196,6 +196,13 @@ re-tokenization has progressed sufficiently.")
 
 (defvar-local racket-hash-lang-mode-lighter "#lang")
 
+(defvar-local racket--hash-lang-doc-family nil
+  "The get-info value for the key documentation-language-family.")
+(defun racket--hash-lang-doc-family ()
+  "Exposed for `racket--describe-search-completing-read' to call."
+  (and (eq major-mode 'racket-hash-lang-mode)
+       racket--hash-lang-doc-family))
+
 (defconst racket--agnostic-syntax-table
   (let ((table (make-syntax-table)))
     ;; From Emacs Lisp Info node "Syntax Table Internals":
@@ -495,6 +502,8 @@ lang's attributes that we care about have changed."
                   (concat "#lang"
                           (when (plist-get plist 'racket-grouping) "()")
                           (when (plist-get plist 'range-indenter) "⇉")))
+      (setq-local racket--hash-lang-doc-family
+                  (plist-get plist 'documentation-language-family))
       ;; Finally run user's module-language-hook.
       (run-hook-with-args 'racket-hash-lang-module-language-hook
                           (plist-get plist 'module-language)))))
diff --git a/racket-scribble.el b/racket-scribble.el
index b427cd2467d..f220be913b7 100644
--- a/racket-scribble.el
+++ b/racket-scribble.el
@@ -1,6 +1,6 @@
 ;;; racket-scribble.el -*- lexical-binding: t -*-
 
-;; Copyright (c) 2021-2024 by Greg Hendershott.
+;; Copyright (c) 2021-2026 by Greg Hendershott.
 ;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc.
 
 ;; Author: Greg Hendershott
@@ -131,20 +131,25 @@ avoid penalty."
        (pcase (dom-attr v 'class)
          ;; Page navigation.
          ("navsettop"
-          (pcase-let* ((navleft (car (dom-by-class v "navleft")))
-                       (top (dom-attr (car (dom-by-tag navleft 'a)) 'href))
-                       (navright (car (dom-by-class v "navright")))
-                       (`(,prev ,up ,next)
-                        (mapcar (lambda (v) (dom-attr v 'href))
-                                (dom-by-tag navright 'a))))
-            (if (and top up)
-                `(racket-nav
-                  ((top  . ,(expand-file-name top racket--scribble-base))
-                   (prev . ,(and prev (expand-file-name prev 
racket--scribble-base)))
-                   (up   . ,(expand-file-name up racket--scribble-base))
-                   (next . ,(and next (expand-file-name next 
racket--scribble-base)))))
-              `(span))))
+          (let* ((navleft (car (dom-by-class v "navleft")))
+                 (top (dom-attr (car (dom-by-tag navleft 'a)) 'href))
+                 (navright (car (dom-by-class v "navright")))
+                 (as (mapcar (lambda (v)
+                               (cons (or (dom-attr v 'rel) "up")
+                                     (dom-attr v 'href)))
+                             (dom-by-tag navright 'a)))
+                 (prev (cdr (assoc "prev" as)))
+                 (up   (cdr (assoc "up" as)))
+                 (next (cdr (assoc "next" as))))
+            `(racket-nav
+              ((top  . ,(and top (expand-file-name top racket--scribble-base)))
+               (prev . ,(and prev (expand-file-name prev 
racket--scribble-base)))
+               (up   . ,(and up (expand-file-name up racket--scribble-base)))
+               (next . ,(and next (expand-file-name next 
racket--scribble-base)))))))
          ("navsetbottom" `(span))
+         ;; Language family
+         ("navfamily"
+          `(racket-family ((data-fam . ,(dom-attr v 'data-fam)))))
          ;; The kind (e.g. "procedure" or "syntax"): Add <hr>
          ("RBackgroundLabel SIEHidden"
           `(div ()
diff --git a/racket/command-server.rkt b/racket/command-server.rkt
index 7df1dff66c2..ff31e5f28a1 100644
--- a/racket/command-server.rkt
+++ b/racket/command-server.rkt
@@ -32,7 +32,7 @@
                                package-details
                                package-op
                                catalog-package-doc-link)]
- ["scribble.rkt"              (bluebox-command doc-search)])
+ ["scribble.rkt"              (bluebox-command doc-search doc-families)])
 
 (provide command-server-loop)
 
@@ -147,6 +147,7 @@
     [`(requires/trim ,path-str)        (requires/trim path-str)]
     [`(requires/base ,path-str)        (requires/base path-str)]
     [`(doc-search ,prefix)             (doc-search prefix)]
+    [`(doc-families)                   (doc-families)]
     [`(hash-lang . ,more)              (apply hash-lang more)]
     [`(pkg-list)                       (package-list)]
     [`(pkg-details ,str)               (package-details str)]
diff --git a/racket/hash-lang-bridge.rkt b/racket/hash-lang-bridge.rkt
index 06a77206ee9..6bc07eced55 100644
--- a/racket/hash-lang-bridge.rkt
+++ b/racket/hash-lang-bridge.rkt
@@ -56,7 +56,9 @@
                                 (cons (symbol->string o) (symbol->string c)))
           'quote-matches      (for/list ([c (in-list (lang-info-quote-matches 
li))])
                                 (make-string 1 c))
-          'comment-delimiters (lang-info-comment-delimiters li))))
+          'comment-delimiters (lang-info-comment-delimiters li)
+          'documentation-language-family
+          (lang-info-documentation-language-family li))))
       (define/override (on-changed-tokens gen beg end)
         (when (< beg end)
           (async-channel-put hash-lang-notify-channel
diff --git a/racket/hash-lang.rkt b/racket/hash-lang.rkt
index 412562370b4..f58794cd3f2 100644
--- a/racket/hash-lang.rkt
+++ b/racket/hash-lang.rkt
@@ -693,6 +693,7 @@
              #f
              #f
              #f
+             #f
              #f))
 
 (define (read-lang-info* in)
@@ -710,7 +711,8 @@
                      (info 'drracket:range-indentation #f)
                      (info 'drracket:range-indentation/reverse-choices #f)
                      (info 'drracket:submit-predicate #f)
-                     (comment-delimiters info mod-lang))
+                     (comment-delimiters info mod-lang)
+                     (info 'documentation-language-family #f))
           end-pos))
 
 ;; Handle the module-language lang info key, as documented at
diff --git a/racket/lang-info.rkt b/racket/lang-info.rkt
index 076de124b58..348d5bf443f 100644
--- a/racket/lang-info.rkt
+++ b/racket/lang-info.rkt
@@ -19,7 +19,8 @@
    range-indenter
    reverse-range-indenter
    submit-predicate
-   comment-delimiters)
+   comment-delimiters
+   documentation-language-family)
   #:transparent #:authentic)
 
 (define racket-grouping-position
diff --git a/racket/scribble.rkt b/racket/scribble.rkt
index c12905422d8..74e498a957e 100644
--- a/racket/scribble.rkt
+++ b/racket/scribble.rkt
@@ -1,4 +1,4 @@
-;; Copyright (c) 2013-2025 by Greg Hendershott.
+;; Copyright (c) 2013-2026 by Greg Hendershott.
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 
 #lang racket/base
@@ -19,6 +19,7 @@
          setup/main-doc
          "define-fallbacks.rkt"
          "lib-pkg.rkt"
+         "safe-dynamic-require.rkt"
          "util.rkt"
          "xref.rkt")
 
@@ -34,6 +35,7 @@
          identifier->bluebox
          bluebox-command
          doc-search
+         doc-families
          module-doc-path
          refresh-doc-index!)
 
@@ -223,8 +225,8 @@
                          (map ~s (exported-index-desc-from-libs desc))])
                       ", "))
        (define fams (match (hash-ref ht 'language-family #f)
-                      [(? list? fams) (string-join (map ~a fams) ", ")]
-                      [#f "Racket"]))
+                      [(? list? fams) fams]
+                      [#f '("Racket")]))
        (define pkg-sort (lib-pkg-sort
                          (match (exported-index-desc-from-libs desc)
                            [(cons lib _) lib]
@@ -249,8 +251,8 @@
               [(or 'lib 'lang 'reader) term]
               [_ (doc-from)])]))
        (define fams (match (hash-ref ht 'language-family #f)
-                      [(? list? fams) (string-join (map ~a fams) ", ")]
-                      [#f "Racket"]))
+                      [(? list? fams) fams]
+                      [#f '("Racket")]))
        (define pkg-sort (lib-pkg-sort
                          (match module-kind
                            ['lib (string->symbol term)]
@@ -277,13 +279,13 @@
                          (match (exported-index-desc-from-libs desc)
                            [(cons lib _) lib]
                            [_            #f])))
-       (values what from "" pkg-sort 0)]
+       (values what from null pkg-sort 0)]
       [(module-path-index-desc? desc)
        (define pkg-sort (lib-pkg-sort (string->symbol term)))
        (values "module" "" "" pkg-sort 0)]
        [else
        (define pkg-sort (lib-pkg-sort #f))
-       (values "documentation" (doc-from) "" pkg-sort 0)]))
+       (values "documentation" (doc-from) null pkg-sort 0)]))
   (list term sort-order what from fams pkg-sort path anchor))
 
 ;; This is for package-details
@@ -293,3 +295,67 @@
     (and (equal? term mod-path-str)
          (equal? what (if lang? "language" "module"))
          (cons path anchor))))
+
+;; Documentation language family information added in Racket 9.1.
+(define doc-families
+  ;; Require some functions both dynamically (in case don't exist in
+  ;; older Racket) and lazily (faster initial load of back end).
+  (let ([get-main-language-family #f]
+        [get-language-families #f]
+        [get-doc-search-dirs #f])
+    (λ ()
+      (define-syntax-rule (req/set! mod fn def-val)
+        (unless fn
+          (set! fn (safe-dynamic-require 'mod 'fn (λ () (λ () def-val))))))
+      (req/set! setup/dirs get-main-language-family "Racket")
+      (req/set! setup/language-family get-language-families null)
+      (req/set! setup/dirs get-doc-search-dirs null)
+      ;; Return cons of main family name, and, list of details for all
+      ;; families. Adjust the details somewhat, from what
+      ;; get-language-families supplies.
+      (define xref (get-xref))
+      (define doc-dirs (get-doc-search-dirs))
+      (cons
+       (get-main-language-family)
+       (for/list ([ht (in-list (get-language-families))])
+         (let* (;; Convert abstract module paths to concrete absolute paths.
+                [ht (for/hash ([(k v) (in-hash ht)])
+                      (cond [(memq k '(doc start-doc describe-doc))
+                             (values k (module-path->absolute-path xref v))]
+                            [else
+                             (values k v)]))]
+                ;; If describe-doc not mapped, use doc.
+                [ht (cond
+                      [(hash-has-key? ht 'describe-doc) ht]
+                      [(hash-ref ht 'doc #f)
+                       => (λ (v) (hash-set ht 'describe-doc v))]
+                      [else ht])]
+                ;; If start-doc not mapped, use doc, else use
+                ;; family-root. See similar logic in
+                ;; send-language-family-page and send-main-page.
+                [ht (cond
+                      [(hash-has-key? ht 'start-doc) ht]
+                      [(hash-ref ht 'doc #f)
+                       => (λ (v) (hash-set ht 'start-doc v))]
+                      [(hash-ref ht 'family-root #f)
+                       =>
+                       (λ (sub)
+                         (or (for/or ([dir (in-list doc-dirs)])
+                               (define path (build-path dir sub "index.html"))
+                               (and (file-exists? path)
+                                    (hash-set ht 'start-doc path)))
+                             ht))]
+                      [else ht])]
+                [ht (hash-remove ht 'doc)])
+           (hash->list ht)))))))
+
+;; Convert mod path => part tag => absolute path.
+;; See send-language-family-page.
+
+(define (module-path->absolute-path xref mp)
+  (define-values (path _anchor)
+    (xref-tag->path+anchor xref (module-path->part-tag mp)))
+  path)
+
+(define (module-path->part-tag mp)
+  `(part (,(format "~a" mp) "top")))

Reply via email to