diff --git a/CHANGELOG.md b/CHANGELOG.md index cf211c021..244088e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v0.30.9 (2023-10-20) + + * Bug fixes + * Fix a scenario where invalid assets would be generated + + * Enhancements + * Add admonition EPUB styles + +## v0.30.8 (2023-10-17) + + * Bug fixes + * Fix regression in umbrella applications + +## v0.30.7 (2023-10-15) + + * Bug fixes + * Do not crash on EDoc type annotations + * Do not crash on functions without name + * Handle remote types in records + * Fix scrolling to top on iOS + * Fix invalid output markup for “hover link” headings + + * Enhancements + * Support any String.Chars as the extra page name + * Improve screen reader accessibility + * Add `:skip_code_autolink_to` option + ## v0.30.6 (2023-08-25) * Enhancements diff --git a/assets/css/_epub.css b/assets/css/_epub.css index ca7ec9fbf..dd5232268 100644 --- a/assets/css/_epub.css +++ b/assets/css/_epub.css @@ -1,6 +1,7 @@ @import 'custom-props/common.css'; @import 'custom-props/theme-light.css'; +@import 'content/epub-admonition.css'; @import 'content/code.css'; @import 'content/functions.css'; @import 'screen-reader.css'; diff --git a/assets/css/content/cheatsheet.css b/assets/css/content/cheatsheet.css index 4c6b81155..dacd19a30 100644 --- a/assets/css/content/cheatsheet.css +++ b/assets/css/content/cheatsheet.css @@ -37,18 +37,14 @@ .page-cheatmd .content-inner h3 { color: var(--main); + text-decoration-color: var(--main); margin: 0 0 1em; font-weight: 400; - overflow: hidden; } .page-cheatmd .content-inner h3 :is(a, a:visited) { color: var(--main); - text-decoration: none; -} - -.page-cheatmd .content-inner h3.section-heading i { - display: none; + text-decoration-color: var(--main); } .page-cheatmd .content-inner section.h3 { @@ -58,7 +54,11 @@ break-inside: avoid; } -.page-cheatmd .content-inner h3::after { +.page-cheatmd .content-inner h3 .text { + overflow: hidden; /* Clips generated content horizontal rule */ +} + +.page-cheatmd .content-inner h3 .text::after { content: ""; margin-left: calc(var(--horizontal-space) / 2); vertical-align: baseline; diff --git a/assets/css/content/epub-admonition.css b/assets/css/content/epub-admonition.css new file mode 100755 index 000000000..45f4a05ca --- /dev/null +++ b/assets/css/content/epub-admonition.css @@ -0,0 +1,76 @@ +.content-inner blockquote:is(.warning, .error, .info, .neutral, .tip) { + border-left: solid 4px; + color: var(--black); + font-size: 0.9em; + line-height: 1.4em; + margin-bottom: 1.5em; + margin-left: 5px; + padding: 7px 15px; + page-break-inside: avoid; +} + +.content-inner blockquote.warning { + background-color: var(--warningBackground); + border-left-color: var(--warningHeadingBackground); +} + +.content-inner blockquote.error { + background-color: var(--errorBackground); + border-left-color: var(--errorHeadingBackground); +} + +.content-inner blockquote.info { + background-color: var(--infoBackground); + border-left-color: var(--infoHeadingBackground); +} + +.content-inner blockquote.neutral { + background-color: var(--neutralBackground); + border-left-color: var(--neutralHeadingBackground); +} + +.content-inner blockquote.tip { + background-color: var(--tipBackground); + border-left-color: var(--tipHeadingBackground); +} + +.content-inner blockquote :is(h3, h4) { + font-weight: bold; + margin: 0px 10px 5px 0px; +} + +.content-inner blockquote :is(h3, h4):is(.warning, .error, .info, .neutral, .tip) { + font-style: normal; + font-weight: 700; +} + +.content-inner blockquote :is(h3, h4).warning { + color: var(--warningHeadingBackground); +} +.content-inner blockquote :is(h3, h4).error { + color: var(--errorHeadingBackground); +} +.content-inner blockquote :is(h3, h4).info { + color: var(--infoHeadingBackground); +} +.content-inner blockquote :is(h3, h4).neutral { + color: var(--neutralHeadingBackground); +} +.content-inner blockquote :is(h3, h4).tip { + color: var(--tipHeadingBackground); +} + +.content-inner blockquote :is(h3, h4):is(.warning, .error, .info, .neutral, .tip) code { + margin: 0 0.5ch; +} + +.content-inner blockquote:is(.warning, .error, .info, .neutral, .tip) code { + background-color: var(--admInlineCodeBackground); + border: 1px solid var(--admInlineCodeBorder); + color: var(--admInlineCode); +} + +.content-inner blockquote:is(.warning, .error, .info, .neutral, .tip) pre code { + background-color: var(--admCodeBackground); + border: 1px solid var(--admCodeBorder); +} diff --git a/assets/css/content/general.css b/assets/css/content/general.css index ca47e16c9..9269244e9 100644 --- a/assets/css/content/general.css +++ b/assets/css/content/general.css @@ -131,30 +131,50 @@ vertical-align: top; } -.content-inner .section-heading a { +.content-inner .section-heading { + --icon-size: 16px; + --icon-spacing: 5px; + display: grid; + grid-template: 1fr / 1fr; +} + +.content-inner .section-heading > :is(.hover-link, .text) { + grid-row: 1; + grid-column: 1; +} + +.content-inner .section-heading .hover-link { text-decoration: none; } .content-inner .section-heading i { - font-size: 16px; + font-size: var(--icon-size); margin-top: .1em; - margin-left: -21px; + margin-left: calc(-1 * (var(--icon-size) + var(--icon-spacing))); + padding-right: var(--icon-spacing); /* Avoids gap in hover area */ opacity: 0; } -.content-inner .section-heading a:is(:hover, :focus) i { - opacity: 1; +@media screen and (max-width: 768px) { + .content-inner .section-heading i { + margin-left: calc(-1 * (var(--icon-size))); + } } -blockquote .section-heading i { +.content-inner blockquote .section-heading i { display: none; } -@media screen and (max-width: 768px) { - .content-inner .section-heading i { - margin-left: -16px; - margin-right: -5px; - } +.content-inner .section-heading .hover-link:is(:hover, :focus) i { + opacity: 1; +} + +/* Allow section link to be hovered and used “through” text */ +.content-inner .section-heading .text { + pointer-events: none; +} +.content-inner .section-heading .text a { + pointer-events: all; } .content-inner .app-vsn { diff --git a/assets/css/copy-button.css b/assets/css/copy-button.css index b90431f88..3cda31f29 100644 --- a/assets/css/copy-button.css +++ b/assets/css/copy-button.css @@ -37,13 +37,9 @@ pre .copy-button:hover svg, pre .copy-button:focus-visible svg { } .copy-button.clicked { - display: block; opacity: 1; color: var(--success); } -.copy-button.clicked::after { - content: "Copied! \2713"; -} .copy-button.clicked svg { display: none; diff --git a/assets/css/custom-props/common.css b/assets/css/custom-props/common.css index d65b0c35d..5104aecb6 100644 --- a/assets/css/custom-props/common.css +++ b/assets/css/custom-props/common.css @@ -20,6 +20,7 @@ --gray200: hsl(210, 29%, 88% ); --gray300: hsl(210, 26%, 84% ); --gray400: hsl(210, 21%, 64% ); + --gray450: hsl(210, 21%, 49% ); --gray500: hsl(210, 21%, 34% ); --gray600: hsl(210, 27%, 26% ); --gray700: hsl(212, 35%, 17% ); diff --git a/assets/css/custom-props/theme-dark.css b/assets/css/custom-props/theme-dark.css index 887f8f537..2262fb7cd 100644 --- a/assets/css/custom-props/theme-dark.css +++ b/assets/css/custom-props/theme-dark.css @@ -10,7 +10,7 @@ body.dark { --linksVisited: var(--gray100); --linksNoUnderline: var(--main-lightened-10); --linksNoUnderlineVisited: var(--main-lightened-05); - --linksDecoration: var(--gray500); + --linksDecoration: var(--gray450); --iconAction: var(--coldGray-lightened-10); --iconActionHover: var(--white); diff --git a/assets/css/custom-props/theme-light.css b/assets/css/custom-props/theme-light.css index 608644ab9..a029dd101 100644 --- a/assets/css/custom-props/theme-light.css +++ b/assets/css/custom-props/theme-light.css @@ -10,7 +10,7 @@ --linksVisited: var(--black); --linksNoUnderline: var(--main-darkened-10); --linksNoUnderlineVisited: var(--main-darkened-20); - --linksDecoration: var(--gray400); + --linksDecoration: var(--gray450); --iconAction: var(--coldGray); --iconActionHover: var(--gray800); diff --git a/assets/css/layout.css b/assets/css/layout.css index 6f68003ff..9b5e27c03 100644 --- a/assets/css/layout.css +++ b/assets/css/layout.css @@ -7,7 +7,7 @@ body { body { --sidebarWidth: 300px; - --sidebarTransitionDuration: .3s; + --sidebarTransitionDuration: 0.3s; background-color: var(--background); color: var(--textBody); font-size: 16px; @@ -57,15 +57,10 @@ body { z-index: 3; } -@media screen and (max-width: 768px) { - .content { - overflow: auto; - scroll-padding-top: 45px; - } -} - body:is(.sidebar-opening, .sidebar-opened) .sidebar-button { - transform: translateX(calc(var(--sidebarWidth) - 100% - var(--sidebarButtonRightOpen))); + transform: translateX( + calc(var(--sidebarWidth) - 100% - var(--sidebarButtonRightOpen)) + ); top: var(--sidebarButtonTopOpen); } @@ -117,7 +112,7 @@ body.sidebar-closed .content { @media screen and (max-width: 768px) { .sidebar-button:before { --sidebarButtonHeight: 60px; - content: ''; + content: ""; display: block; z-index: -1; pointer-events: none; @@ -145,13 +140,13 @@ body.sidebar-closed .content { body.search-focused .sidebar-button { transform: translateX(250px) scaleY(0); - transition: all .15s ease-out; + transition: all 0.15s ease-out; opacity: 0; } body.search-focused .sidebar-search .search-close-button { transform: scaleY(1); - transition: transform .15s ease-out .15s; + transition: transform 0.15s ease-out 0.15s; } .content-inner { diff --git a/assets/css/search.css b/assets/css/search.css index cefeb7f56..3ecf1e8d0 100644 --- a/assets/css/search.css +++ b/assets/css/search.css @@ -6,10 +6,9 @@ #search .loading { height: 64px; width: 64px; - vertical-align: middle; position: absolute; top: 50%; - left: calc(18%); + left: calc(50% - 32px); } #search .loading div { diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css index 9a096eba6..3df2f0cdc 100644 --- a/assets/css/sidebar.css +++ b/assets/css/sidebar.css @@ -94,11 +94,10 @@ color: initial; } -.sidebar .sidebar-projectVersion form::after { +.sidebar .sidebar-projectVersionsDropdownCaret { position: absolute; left: 0; top: 2px; - content: "\25bc"; z-index: 1; font-size: 8px; color: var(--sidebarMuted); @@ -114,7 +113,7 @@ margin: 0; } -.sidebar .sidebar-listNav :is(li, li a) { +.sidebar .sidebar-listNav :is(li, li button) { text-transform: uppercase; font-size: 14px; color: var(--sidebarMuted); @@ -125,18 +124,26 @@ padding: 0; } -.sidebar .sidebar-listNav li a { +.sidebar .sidebar-listNav li button { + background: none; + border: 0; + border-radius: 0; + -webkit-appearance: none; + text-align: inherit; + color: inherit; + font-weight: inherit; + cursor: pointer; display: inline-block; line-height: 27px; border-bottom: 3px solid transparent; padding: 0 10px; } -.sidebar .sidebar-listNav li:is(:hover, .selected) a { +.sidebar .sidebar-listNav li:is(:hover, .selected) button { border-color: var(--sidebarLanguageAccentBar); } -.sidebar .sidebar-listNav li:is(:hover, .selected) a { +.sidebar .sidebar-listNav li:is(:hover, .selected) button { color: var(--sidebarAccentMain); } @@ -211,38 +218,42 @@ font-weight: bold; } -.sidebar #full-list { - margin: 0; - padding: 20px 0; +.sidebar .sidebar-tabpanel { + flex: 1 1 .01%; overflow-y: auto; overscroll-behavior: contain; position: relative; -webkit-overflow-scrolling: touch; - flex: 1 1 .01%; } -.sidebar #full-list :is(li, a) { +.sidebar .full-list { + margin: 0; + padding: 20px 0; + position: relative; +} + +.sidebar .full-list :is(li, a) { overflow: hidden; text-overflow: ellipsis; } -.sidebar #full-list li { +.sidebar .full-list li { padding: 0; margin-right: 30px; line-height: 27px; white-space: nowrap; } -.sidebar #full-list li.docs { +.sidebar .full-list li.docs { margin-right: 0; } -.sidebar #full-list li.open > ul { +.sidebar .full-list li.open > ul { display: block; margin-left: 10px; } -.sidebar #full-list li a.expand + button.icon-expand { +.sidebar .full-list li a.expand + button.icon-expand { appearance: none; background-color: transparent; border: 0; @@ -258,37 +269,37 @@ transform: translateY(calc(-100% - 4px)); } -.sidebar #full-list li a + button.icon-expand:after { +.sidebar .full-list li a + button.icon-expand:after { font-family: remixicon; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.sidebar #full-list li a.expand + button.icon-expand:after { +.sidebar .full-list li a.expand + button.icon-expand:after { content: var(--icon-arrow-down-s); } -.sidebar #full-list li.open > a.expand + button.icon-expand:after { +.sidebar .full-list li.open > a.expand + button.icon-expand:after { content: var(--icon-arrow-up-s); } -.sidebar #full-list li.docs > a + button.icon-expand { +.sidebar .full-list li.docs > a + button.icon-expand { margin-right: 12px; font-size: var(--sidebarFontSize); line-height: var(--sidebarFontSize); transform: translateY(calc(-100% - 5px)); } -.sidebar #full-list li.docs > a + button.icon-expand:after { +.sidebar .full-list li.docs > a + button.icon-expand:after { content: var(--icon-add); } -.sidebar #full-list li.docs.open > a + button.icon-expand:after { +.sidebar .full-list li.docs.open > a + button.icon-expand:after { content: var(--icon-subtract); } -.sidebar #full-list li.nesting-context { +.sidebar .full-list li.nesting-context { font-weight: bold; font-size: .9em; line-height: 1.8em; @@ -296,7 +307,7 @@ padding-left: 15px; } -.sidebar #full-list li.group { +.sidebar .full-list li.group { text-transform: uppercase; font-weight: bold; font-size: .8em; @@ -306,58 +317,58 @@ padding-left: 15px; } -.sidebar #full-list li a { +.sidebar .full-list li a { padding: 3px 0 3px 15px; color: var(--sidebarItem); } -.sidebar #full-list > li > a { +.sidebar .full-list > li > a { display: block; width: 100%; height: 27px; line-height: var(--sidebarLineHeight); } -.sidebar #full-list li .current-section > a { +.sidebar .full-list li .current-section > a { color: var(--sidebarActiveItem); } -.sidebar #full-list li .current-section > a + button.icon-expand { +.sidebar .full-list li .current-section > a + button.icon-expand { color: var(--sidebarActiveItem); } -.sidebar #full-list > li > a:hover { +.sidebar .full-list > li > a:hover { border-left: 3px solid var(--sidebarLanguageAccentBar); padding-left: 12px; } -.sidebar #full-list > li.current-page > a { +.sidebar .full-list > li.current-page > a { color: var(--sidebarActiveItem); border-left: 3px solid var(--sidebarLanguageAccentBar); padding-left: 12px; } -.sidebar #full-list > li.current-page > a:after, -.sidebar #full-list > li.current-page { +.sidebar .full-list > li.current-page > a:after, +.sidebar .full-list > li.current-page { color: var(--sidebarActiveItem); } -.sidebar #full-list > li:last-child { +.sidebar .full-list > li:last-child { margin-bottom: 30px; } -.sidebar #full-list > li.group:first-child { +.sidebar .full-list > li.group:first-child { margin-top: 0; } -.sidebar #full-list ul { +.sidebar .full-list ul { display: none; margin: 10px 15px; margin-right: 0; padding: 0; } -.sidebar #full-list ul li { +.sidebar .full-list ul li { font-weight: 300; line-height: var(--sidebarFontSize); padding: 0 8px; @@ -365,65 +376,65 @@ color: var(--sidebarAccentMain); } -.non-apple-os .sidebar #full-list ul li { +.non-apple-os .sidebar .full-list ul li { font-weight: 400; /* Non-Apple OSes render small light type too thinly */ } -.sidebar #full-list ul li.current-hash { +.sidebar .full-list ul li.current-hash { color: var(--sidebarActiveItem); } -.sidebar #full-list ul li.current-hash > a { +.sidebar .full-list ul li.current-hash > a { color: var(--sidebarActiveItem); } -.sidebar #full-list ul li.current-hash > a:before, -.sidebar #full-list > li > ul > li > a:hover:before { +.sidebar .full-list ul li.current-hash > a:before, +.sidebar .full-list > li > ul > li > a:hover:before { content: "\2022"; position: absolute; margin-left: -15px; color: var(--sidebarActiveItem); } -.sidebar #full-list ul li a { +.sidebar .full-list ul li a { padding-left: 15px; display: block; width: 100%; height: 24px; } -.sidebar #full-list ul li ul { +.sidebar .full-list ul li ul { display: none; margin: 9px 20px; margin-right: 0; } -.sidebar #full-list ul li ul li { +.sidebar .full-list ul li ul li { margin-right: 0; height: 20px; color: var(--sidebarAccentMain); } -.sidebar #full-list ul li ul li a { +.sidebar .full-list ul li ul li a { border-left: 1px solid var(--sidebarInactiveItemMarker); padding: 0 10px; height: 20px; } -.sidebar #full-list ul li ul li.current-hash > a:before { +.sidebar .full-list ul li ul li.current-hash > a:before { content: none; } -.sidebar #full-list ul li ul li > a:hover { +.sidebar .full-list ul li ul li > a:hover { border-color: var(--sidebarLanguageAccentBar); } -.sidebar #full-list ul li ul li.current-hash > a { +.sidebar .full-list ul li ul li.current-hash > a { color: var(--sidebarActiveItem); border-color: var(--sidebarLanguageAccentBar); } -.sidebar #full-list ul li ul li.current-hash > a { +.sidebar .full-list ul li ul li.current-hash > a { color: var(--sidebarActiveItem); margin-left: 0; } @@ -466,7 +477,7 @@ overflow-y: auto; } - .sidebar #full-list { + .sidebar .full-list { overflow: visible; } } diff --git a/assets/js/content.js b/assets/js/content.js index 0058233e6..082537c18 100644 --- a/assets/js/content.js +++ b/assets/js/content.js @@ -30,7 +30,7 @@ function fixLinks () { * Add CSS classes to `blockquote` elements when those are used to * support admonition text blocks */ -function fixBlockquotes () { +export function fixBlockquotes () { const classes = ['warning', 'info', 'error', 'neutral', 'tip'] classes.forEach(element => { @@ -81,3 +81,23 @@ function getLivebookDevRunUrl (notebookUrl) { function getLivebookImportUrl (livebookUrl, notebookUrl) { return `${livebookUrl}/import?url=${encodeURIComponent(notebookUrl)}` } + +// Check if the device width is below a certain threshold (e.g., 768px for mobile) +document.addEventListener('click', function (e) { + if (window.innerWidth <= 768) { + const target = e.target.closest('a[href^="#"]') + if (target) { + e.preventDefault() + const targetId = target.getAttribute('href').substring(1) + const targetElement = document.getElementById(targetId) + if (targetElement) { + const offset = 45 // Adjust this offset as needed + const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset + window.scrollTo({ + top: targetPosition, + behavior: 'smooth' + }) + } + } + } +}) diff --git a/assets/js/copy-button.js b/assets/js/copy-button.js index c5cfcb7fc..b2ec9ebeb 100644 --- a/assets/js/copy-button.js +++ b/assets/js/copy-button.js @@ -1,6 +1,6 @@ import { qsAll } from './helpers' -const BUTTON = '' +const BUTTON = '' /** * Initializes copy buttons. @@ -22,6 +22,7 @@ function addCopyButtons () { Array.from(qsAll('.copy-button')).forEach(button => { let timeout button.addEventListener('click', () => { + const ariaLiveContent = button.querySelector('[aria-live]') timeout && clearTimeout(timeout) const text = @@ -32,7 +33,11 @@ function addCopyButtons () { navigator.clipboard.writeText(text) button.classList.add('clicked') - timeout = setTimeout(() => button.classList.remove('clicked'), 3000) + ariaLiveContent.innerHTML = 'Copied! ✓' + timeout = setTimeout(() => { + button.classList.remove('clicked') + ariaLiveContent.innerHTML = '' + }, 3000) }) }) } diff --git a/assets/js/entry/epub.js b/assets/js/entry/epub.js index ca2080c52..abf37e456 100644 --- a/assets/js/entry/epub.js +++ b/assets/js/entry/epub.js @@ -1,3 +1,8 @@ +import { onDocumentReady } from '../helpers' +import { fixBlockquotes } from '../content' import { initialize as initMakeup } from '../makeup' -initMakeup() +onDocumentReady(() => { + initMakeup() + fixBlockquotes() +}) diff --git a/assets/js/handlebars/templates/modal-layout.handlebars b/assets/js/handlebars/templates/modal-layout.handlebars index 966ab405f..4ecb2d40f 100644 --- a/assets/js/handlebars/templates/modal-layout.handlebars +++ b/assets/js/handlebars/templates/modal-layout.handlebars @@ -2,7 +2,7 @@ - + <%# Extra content specified by the user (e.g. custom Javascript) %> <%= before_closing_body_tag(config, :html) %> diff --git a/lib/ex_doc/formatter/html/templates/module_template.eex b/lib/ex_doc/formatter/html/templates/module_template.eex index 195caef2b..eb60636dd 100644 --- a/lib/ex_doc/formatter/html/templates/module_template.eex +++ b/lib/ex_doc/formatter/html/templates/module_template.eex @@ -33,8 +33,8 @@

- Summary + Summary

<%= for {name, nodes} <- summary, do: summary_template(name, nodes) %> @@ -45,8 +45,8 @@

- <%= name %> + <%= name %>

<%= for node <- nodes, do: detail_template(node, module) %> diff --git a/lib/ex_doc/formatter/html/templates/sidebar_template.eex b/lib/ex_doc/formatter/html/templates/sidebar_template.eex index 41872181b..ac0390c40 100644 --- a/lib/ex_doc/formatter/html/templates/sidebar_template.eex +++ b/lib/ex_doc/formatter/html/templates/sidebar_template.eex @@ -1,21 +1,21 @@
- -
-
- - + + + <%= if nodes_map.modules != [] do %> + + <% end %> + + <%= if nodes_map.tasks != [] do %> + + <% end %> + + -
+
diff --git a/lib/ex_doc/language/elixir.ex b/lib/ex_doc/language/elixir.ex index 172a5d49d..e64ac42ad 100644 --- a/lib/ex_doc/language/elixir.ex +++ b/lib/ex_doc/language/elixir.ex @@ -96,7 +96,7 @@ defmodule ExDoc.Language.Elixir do # If it is none, then we need to look at underscore. # TODO: We can remove this on Elixir v1.13 as all underscored are hidden. defp doc?({{_, name, _}, _, _, :none, _}, _type) do - hd(Atom.to_charlist(name)) != ?_ + not match?([?_ | _], Atom.to_charlist(name)) end # Everything else is hidden. @@ -559,6 +559,14 @@ defmodule ExDoc.Language.Elixir do end defp url(string, mode, config) do + if Enum.any?(config.skip_code_autolink_to, &(&1 == string)) do + nil + else + parse_url(string, mode, config) + end + end + + defp parse_url(string, mode, config) do case Regex.run(~r{^(.+)/(\d+)$}, string) do [_, left, right] -> with {:ok, arity} <- parse_arity(right) do @@ -775,12 +783,16 @@ defmodule ExDoc.Language.Elixir do (\(.*\)) # Arguments }x - Regex.replace(regex, string, fn _all, call_string, module_string, name_string, rest -> + Regex.replace(regex, string, fn all, call_string, module_string, name_string, rest -> module = string_to_module(module_string) name = String.to_atom(name_string) arity = count_args(rest, 0, 0) original_text = call_string <> "()" + if Enum.any?(config.filtered_modules, &(&1.id == module_string)) do + warn("Typespec references filtered module: #{all}", {config.file, config.line}, config.id) + end + url = if module do remote_url({:type, module, name, arity}, config, original_text) diff --git a/lib/ex_doc/language/erlang.ex b/lib/ex_doc/language/erlang.ex index 2363813ff..0d1ce48da 100644 --- a/lib/ex_doc/language/erlang.ex +++ b/lib/ex_doc/language/erlang.ex @@ -215,6 +215,10 @@ defmodule ExDoc.Language.Erlang do binary end + defp walk_doc({:code, _attrs, [{:a, _, _, _}], _meta} = ast, _config) do + ast + end + defp walk_doc({:code, attrs, [code], meta} = ast, config) do {text, url} = case parse_autolink(code) do @@ -583,6 +587,10 @@ defmodule ExDoc.Language.Erlang do {:->, _, [[{name, _, _}], {:any, _, _}]}, acc when name == :... -> {nil, acc} + # record{type :: remote:type/arity} + {:field_type, _, [name, {{:., _, [r_mod, r_type]}, _, args}]}, acc -> + {name, [{pp({r_mod, r_type}), {r_mod, r_type, length(args)}} | acc]} + # #{x :: t()} {:field_type, _, [name, type]}, acc when is_atom(name) -> {type, acc} diff --git a/lib/ex_doc/nodes.ex b/lib/ex_doc/nodes.ex index 263ac60ff..195775641 100644 --- a/lib/ex_doc/nodes.ex +++ b/lib/ex_doc/nodes.ex @@ -22,7 +22,8 @@ defmodule ExDoc.ModuleNode do source_url: nil, type: nil, language: nil, - annotations: [] + annotations: [], + metadata: nil @typep annotation :: atom() @@ -46,7 +47,8 @@ defmodule ExDoc.ModuleNode do source_url: String.t() | nil, type: atom(), language: module(), - annotations: [annotation()] + annotations: [annotation()], + metadata: map() } end diff --git a/lib/ex_doc/refs.ex b/lib/ex_doc/refs.ex index 4a7dac33f..b09b3b391 100644 --- a/lib/ex_doc/refs.ex +++ b/lib/ex_doc/refs.ex @@ -116,7 +116,7 @@ defmodule ExDoc.Refs do defguardp has_no_docs(doc) when doc == :none or doc == %{} - defp starts_with_underscore?(name), do: hd(Atom.to_charlist(name)) == ?_ + defp starts_with_underscore?(name), do: match?([?_ | _], Atom.to_charlist(name)) defp visibility(:hidden), do: :hidden diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index 59abbb1c0..7a5bcf509 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -12,29 +12,59 @@ defmodule ExDoc.Retriever do @doc """ Extract documentation from all modules in the specified directory or directories. + + Returns a tuple containing `{modules, filtered}`, using `config.filter_modules` + as a filter criteria. """ - @spec docs_from_dir(Path.t() | [Path.t()], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()] + @spec docs_from_dir(Path.t() | [Path.t()], ExDoc.Config.t()) :: + {[ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()]} def docs_from_dir(dir, config) when is_binary(dir) do - files = Path.wildcard(Path.expand("*.beam", dir)) - - files - |> Enum.map(&filename_to_module/1) - |> docs_from_modules(config) + dir + |> docs_from_dir({[], []}, config) + |> sort_modules(config) end def docs_from_dir(dirs, config) when is_list(dirs) do - Enum.flat_map(dirs, &docs_from_dir(&1, config)) + dirs + |> Enum.reduce({[], []}, &docs_from_dir(&1, &2, config)) |> sort_modules(config) end + defp docs_from_dir(dir, acc, config) do + files = Path.wildcard(Path.expand("*.beam", dir)) + + files + |> Enum.map(&filename_to_module/1) + |> docs_from_modules(acc, config) + end + @doc """ - Extract documentation from all modules in the list `modules` + Extract documentation from all modules and returns a tuple containing + `{modules, filtered}`, two lists of modules that were extracted and filtered + by `config.filter_modules`, respectively. """ - @spec docs_from_modules([atom], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()] + @spec docs_from_modules([atom], ExDoc.Config.t()) :: + {[ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()]} def docs_from_modules(modules, config) when is_list(modules) do - modules - |> Enum.flat_map(&get_module(&1, config)) - |> sort_modules(config) + modules |> docs_from_modules({[], []}, config) |> sort_modules(config) + end + + defp docs_from_modules(modules, acc, config) do + Enum.reduce(modules, acc, fn module_name, {modules, filtered} = acc -> + case get_module(module_name, config) do + {:error, _module} -> + acc + + {:ok, module_node} -> + if config.filter_modules.(module_node.module, module_node.metadata), + do: {[module_node | modules], filtered}, + else: {modules, [module_node | filtered]} + end + end) + end + + defp sort_modules({modules, filtered}, config) do + {sort_modules(modules, config), sort_modules(filtered, config)} end defp sort_modules(modules, config) when is_list(modules) do @@ -50,14 +80,13 @@ defmodule ExDoc.Retriever do end defp get_module(module, config) do - with {:docs_v1, _, language, _, _, metadata, _} = docs_chunk <- docs_chunk(module), - true <- config.filter_modules.(module, metadata), + with {:docs_v1, _, language, _, _, _metadata, _} = docs_chunk <- docs_chunk(module), {:ok, language} <- ExDoc.Language.get(language, module), %{} = module_data <- language.module_data(module, docs_chunk, config) do - [generate_node(module, module_data, config)] + {:ok, generate_node(module, module_data, config)} else _ -> - [] + {:error, module} end end @@ -142,7 +171,8 @@ defmodule ExDoc.Retriever do source_path: source_path, source_url: source_link(source, module_data.line), language: module_data.language, - annotations: List.wrap(metadata[:tags]) + annotations: List.wrap(metadata[:tags]), + metadata: metadata } end diff --git a/lib/mix/tasks/docs.ex b/lib/mix/tasks/docs.ex index 7712dcb64..5c9e44e6a 100644 --- a/lib/mix/tasks/docs.ex +++ b/lib/mix/tasks/docs.ex @@ -10,11 +10,11 @@ defmodule Mix.Tasks.Docs do ## Command line options * `--canonical`, `-n` - Indicate the preferred URL with - rel="canonical" link element, defaults to no canonical path + `rel="canonical"` link element, defaults to no canonical path - * `--formatter`, `-f` - Which formatters to use, "html" or - "epub". This option can be given more than once. By default, - both html and epub are generated. + * `--formatter`, `-f` - Which formatters to use, `html` or + `epub`. This option can be given more than once. By default, + both `html` and `epub` are generated. * `--language` - Specifies the language to annotate the EPUB output in valid [BCP 47](https://tools.ietf.org/html/bcp47) @@ -24,8 +24,8 @@ defmodule Mix.Tasks.Docs do * `--output`, `-o` - Output directory for the generated docs, default: `"doc"` - * `--proglang` - Chooses the main programming language: "elixir" - or "erlang" + * `--proglang` - Chooses the main programming language: `elixir` + or `erlang` The command line options have higher precedence than the options specified in your `mix.exs` file below. @@ -148,6 +148,12 @@ defmodule Mix.Tasks.Docs do skip the warnings, for a given module/function/callback/type (e.g.: `["Foo", "Bar.baz/0"]`) or on a given file (e.g.: `["pages/deprecations.md"]`); default: `[]`. + * `:skip_code_autolink_to` - Similar to `:skip_undefined_reference_warnings_on`, this option + controls which terms will be skipped by ExDoc when building documentation. + Useful for example if you want to highlight private modules or functions + without warnings (e.g.: `["PrivateModule", "PrivateModule.func/1"]`); + default: `[]`. + * `:source_beam` - Path to the beam directory; default: mix's compile path. * `:source_ref` - The branch/commit/tag used for source link inference; diff --git a/mix.exs b/mix.exs index ba26772e7..1aed6c84c 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule ExDoc.Mixfile do use Mix.Project @source_url "https://github.com/elixir-lang/ex_doc" - @version "0.30.6" + @version "0.30.9" def project do [ diff --git a/test/ex_doc/formatter/epub/templates_test.exs b/test/ex_doc/formatter/epub/templates_test.exs index 7eed7648c..7754c004e 100644 --- a/test/ex_doc/formatter/epub/templates_test.exs +++ b/test/ex_doc/formatter/epub/templates_test.exs @@ -27,8 +27,8 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do defp get_module_page(names, config \\ []) do config = doc_config(config) - mods = ExDoc.Retriever.docs_from_modules(names, config) - [mod | _] = HTML.render_all(mods, ".xhtml", config, highlight_tag: "samp") + {mods, []} = ExDoc.Retriever.docs_from_modules(names, config) + [mod | _] = HTML.render_all(mods, [], ".xhtml", config, highlight_tag: "samp") Templates.module_page(config, mod) end @@ -59,11 +59,13 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do assert content =~ ~r{CompiledWithDocs [^<]*} assert content =~ ~r{

\s*CompiledWithDocs\s*} + assert content =~ ~s{

Summary

} + assert content =~ - ~r{

.*.*.*Example.*.*

}ms + ~r{

.*Example.*

}ms assert content =~ - ~r{

.*.*.*Example H3 heading.*.*

}ms + ~r{

.*Example H3 heading.*

}ms assert content =~ ~r{moduledoc.*Example.*CompiledWithDocs\.example.*}ms @@ -85,8 +87,8 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do ] ) - assert content =~ ~r{id="example-functions".*href="#example-functions".*Example functions}ms - assert content =~ ~r{id="legacy".*href="#legacy".*Legacy}ms + assert content =~ ~r{id="example-functions".*Example functions}ms + assert content =~ ~r{id="legacy".*Legacy}ms assert content =~ ~r{id="example-functions".*id="example/2"}ms refute content =~ ~r{id="legacy".*id="example/2"}ms refute content =~ ~r{id="functions".*id="example/2"}ms diff --git a/test/ex_doc/formatter/epub_io_test.exs b/test/ex_doc/formatter/epub_io_test.exs deleted file mode 100644 index 086374790..000000000 --- a/test/ex_doc/formatter/epub_io_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -defmodule ExDoc.Formatter.EPUBIOTest do - use ExUnit.Case, async: false - - @moduletag :tmp_dir - - test "succeeds if trying to write into an empty existing directory", %{tmp_dir: tmp_dir} do - File.mkdir!("#{tmp_dir}/doc") - - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) == "" - end - - test "warns if trying to write into existing directory with files", %{tmp_dir: tmp_dir} do - File.mkdir!("#{tmp_dir}/doc") - File.touch!("#{tmp_dir}/doc/foo.txt") - - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) =~ "ExDoc is outputting to an existing directory" - - # Warn only once - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) == "" - end - - defp generate_docs(tmp_dir) do - config = [ - app: :foo, - formatter: "epub", - output: "#{tmp_dir}/doc", - source_beam: "#{tmp_dir}/ebin" - ] - - ExDoc.generate_docs("Foo", "1.0.0", config) - end -end diff --git a/test/ex_doc/formatter/html/erlang_test.exs b/test/ex_doc/formatter/html/erlang_test.exs index d993c9278..92556efc2 100644 --- a/test/ex_doc/formatter/html/erlang_test.exs +++ b/test/ex_doc/formatter/html/erlang_test.exs @@ -5,19 +5,13 @@ defmodule ExDoc.Formatter.HTML.ErlangTest do @moduletag :otp_eep48 @moduletag :tmp_dir - setup %{tmp_dir: tmp_dir} do - output = tmp_dir <> "/doc" - File.mkdir!(output) - File.touch!("#{output}/.ex_doc") - end - test "smoke test", c do erlc(c, :foo, ~S""" %% @doc %% foo module. -module(foo). -export([foo/1, bar/0]). - -export_type([t/0]). + -export_type([t/0, t2/0]). %% @doc %% f/0 function. @@ -29,6 +23,10 @@ defmodule ExDoc.Formatter.HTML.ErlangTest do -type t() :: atom(). %% t/0 type. + + -record(rec, {k1 :: any(), k2 :: any()}). + + -type t2() :: #rec{k1 :: uri_string:uri_string(), k2 :: uri_string:uri_string() | undefined}. """) doc = generate_docs(c) @@ -38,6 +36,12 @@ defmodule ExDoc.Formatter.HTML.ErlangTest do assert "-type t() :: atom()." = doc |> Floki.find("pre:fl-contains('t() :: atom().')") |> Floki.text() + + assert "-type t2() :: #rec{k1 :: uri_string:uri_string(), k2 :: uri_string:uri_string() | undefined}." + + doc + |> Floki.find("pre:fl-contains('t2() ::')") + |> Floki.text() end defp generate_docs(c) do diff --git a/test/ex_doc/formatter/html/search_data_test.exs b/test/ex_doc/formatter/html/search_data_test.exs index 9cfda6fb3..a04af1f67 100644 --- a/test/ex_doc/formatter/html/search_data_test.exs +++ b/test/ex_doc/formatter/html/search_data_test.exs @@ -4,12 +4,6 @@ defmodule ExDoc.Formatter.HTML.SearchDataTest do @moduletag :tmp_dir - setup %{tmp_dir: tmp_dir} do - output = tmp_dir <> "/doc" - File.mkdir!(output) - File.touch!("#{output}/.ex_doc") - end - test "Elixir module", c do modules = elixirc(c, ~S''' @@ -243,9 +237,9 @@ defmodule ExDoc.Formatter.HTML.SearchDataTest do end defp search_data(modules, config) do - modules = ExDoc.Retriever.docs_from_modules(modules, config) + {modules, []} = ExDoc.Retriever.docs_from_modules(modules, config) - ExDoc.Formatter.HTML.run(modules, config) + ExDoc.Formatter.HTML.run(modules, [], config) [path] = Path.wildcard(Path.join([config.output, "dist", "search_data-*.js"])) "searchData=" <> json = File.read!(path) Jason.decode!(json) diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs index f0912bc07..4b6e5d0d7 100644 --- a/test/ex_doc/formatter/html/templates_test.exs +++ b/test/ex_doc/formatter/html/templates_test.exs @@ -31,8 +31,8 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do defp get_module_page(names, context, config \\ []) do config = doc_config(context, config) - mods = ExDoc.Retriever.docs_from_modules(names, config) - [mod | _] = HTML.render_all(mods, ".html", config, []) + {mods, []} = ExDoc.Retriever.docs_from_modules(names, config) + [mod | _] = HTML.render_all(mods, [], ".html", config, []) Templates.module_page(mod, @empty_nodes_map, config) end @@ -47,61 +47,61 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "generates headers with hovers" do assert Templates.link_headings("

Foo

Bar

") == """

- + - Foo + Foo

- + - Bar + Bar

""" assert Templates.link_headings("

Foo

\n

Bar

") == """

- + - Foo + Foo

- + - Bar + Bar

""" assert Templates.link_headings("

Bar

") == """

- + - Bar + Bar

""" assert Templates.link_headings("

\n

Bar

") == """

- + - Bar + Bar

""" assert Templates.link_headings("

Foo

") == String.trim_trailing("""

- + - Foo + Foo

""") @@ -109,10 +109,10 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do assert Templates.link_headings("

Foo

\n

") == String.trim_trailing("""

- + - Foo + Foo

@@ -120,10 +120,10 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do assert Templates.link_headings("

Foo

") == """

- + - Foo + Foo

""" end @@ -131,17 +131,17 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "generates headers with unique id's" do assert Templates.link_headings("

Foo

\n

Foo

") == """

- + - Foo + Foo

- + - Foo + Foo

""" end @@ -153,10 +153,10 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do assert Templates.link_headings(admonition_block) == """

- + - Foo + Foo

""" @@ -242,9 +242,14 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do tasks: [] }) - assert content =~ ~r{
  • Modules
  • } - refute content =~ ~r{
  • Exceptions
  • } - refute content =~ ~r{
  • Mix Tasks
  • } + assert content =~ + ~r{
  • [\s\n]*[\s\n]*
  • } + + assert content =~ + ~r{} + + refute content =~ ~r{id="tasks-list-tab-button"} + refute content =~ ~r{id="tasks-full-list"} end test "display built with footer by proglang option", context do @@ -264,7 +269,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "outputs listing for the given nodes", context do names = [CompiledWithDocs, CompiledWithDocs.Nested] - nodes = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) + {nodes, []} = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) assert [ %{ @@ -293,7 +298,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "outputs deprecated: true if node is deprecated", context do names = [CompiledWithDocs] - nodes = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) + {nodes, []} = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) path = ["modules", Access.at!(0), "nodeGroups", Access.at!(0), "nodes"] sidebar_functions = get_in(create_sidebar_items(%{modules: nodes}, []), path) @@ -306,7 +311,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "outputs deprecated: true if module is deprecated", context do names = [Warnings] - nodes = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) + {nodes, []} = ExDoc.Retriever.docs_from_modules(names, doc_config(context)) assert Enum.any?( create_sidebar_items(%{modules: nodes}, [])["modules"], @@ -315,7 +320,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do end test "outputs nodes grouped based on metadata", context do - nodes = + {nodes, []} = ExDoc.Retriever.docs_from_modules( [CompiledWithDocs, CompiledWithDocs.Nested], doc_config(context, @@ -361,7 +366,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "outputs module groups for the given nodes", context do names = [CompiledWithDocs, CompiledWithDocs.Nested] group_mapping = [groups_for_modules: [Group: [CompiledWithDocs]]] - nodes = ExDoc.Retriever.docs_from_modules(names, doc_config(context, group_mapping)) + {nodes, []} = ExDoc.Retriever.docs_from_modules(names, doc_config(context, group_mapping)) assert [ %{"group" => ""}, @@ -420,8 +425,8 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "builds sections out of moduledocs", context do names = [CompiledWithDocs, CompiledWithoutDocs, DuplicateHeadings] config = doc_config(context) - nodes = ExDoc.Retriever.docs_from_modules(names, config) - nodes = HTML.render_all(nodes, ".html", config, []) + {nodes, []} = ExDoc.Retriever.docs_from_modules(names, config) + nodes = HTML.render_all(nodes, [], ".html", config, []) [compiled_with_docs, compiled_without_docs, duplicate_headings] = create_sidebar_items(%{modules: nodes}, [])["modules"] @@ -471,10 +476,10 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do ~r{moduledoc.*Example.*CompiledWithDocs\.example.*}ms assert content =~ - ~r{

    .*.*.*Example.*.*

    }ms + ~r{

    .*.*.*.*Example ☃ Unicode > escaping.*

    }ms assert content =~ - ~r{

    .*.*.*Example H3 heading.*.*

    }ms + ~r{

    .*.*.*.*Example H3 heading.*

    }ms # Summaries assert content =~ ~r{example/2.*Some example}ms @@ -567,7 +572,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do content = get_module_page([CompiledWithDocs], context) assert content =~ - ~r{

    .*.*.*Examples.*.*

    }ms + ~r{

    .*.*.*.*Examples.*

    }ms end test "do not output overlapping functions, causing duplicate IDs", context do diff --git a/test/ex_doc/formatter/html_io_test.exs b/test/ex_doc/formatter/html_io_test.exs deleted file mode 100644 index 8da38c2b2..000000000 --- a/test/ex_doc/formatter/html_io_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -defmodule ExDoc.Formatter.HtmlIOTest do - use ExUnit.Case, async: false - - @moduletag :tmp_dir - - test "succeeds if trying to write into an empty existing directory", %{tmp_dir: tmp_dir} do - File.mkdir!("#{tmp_dir}/doc") - - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) == "" - end - - test "warns if trying to write into existing directory with files", %{tmp_dir: tmp_dir} do - File.mkdir!("#{tmp_dir}/doc") - File.touch!("#{tmp_dir}/doc/foo.txt") - - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) =~ "ExDoc is outputting to an existing directory" - - # Warn only once - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - generate_docs(tmp_dir) - end) == "" - end - - defp generate_docs(tmp_dir) do - config = [ - app: :foo, - formatter: "html", - output: "#{tmp_dir}/doc", - source_beam: "#{tmp_dir}/ebin" - ] - - ExDoc.generate_docs("Foo", "1.0.0", config) - end -end diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index 03490d5f4..f4e2f7382 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -6,12 +6,6 @@ defmodule ExDoc.Formatter.HTMLTest do @moduletag :tmp_dir - setup %{tmp_dir: tmp_dir} do - output = tmp_dir <> "/html" - File.mkdir_p!(output) - File.touch!(output <> "/.ex_doc") - end - defp read_wildcard!(path) do [file] = Path.wildcard(path) File.read!(file) @@ -365,10 +359,10 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{README [^<]*} assert content =~ - ~r{

    .*.*.*Header sample.*.*

    }ms + ~r{

    .*.*.*.*Header sample.*

    }ms assert content =~ - ~r{

    .*.*.*more > than.*.*

    }ms + ~r{

    .*.*.*.*more > than.*

    }ms assert content =~ ~r{RandomError} @@ -519,7 +513,7 @@ defmodule ExDoc.Formatter.HTMLTest do doc_config(context, extras: [ "test/fixtures/PlainTextFiles.md", - "test/fixtures/LICENSE": [filename: "linked-license"], + {"test/fixtures/LICENSE", filename: "linked-license"}, "test/fixtures/PlainText.txt": [filename: "plain_text"] ] ) diff --git a/test/ex_doc/language/elixir_test.exs b/test/ex_doc/language/elixir_test.exs index e0bd260c0..01a635b64 100644 --- a/test/ex_doc/language/elixir_test.exs +++ b/test/ex_doc/language/elixir_test.exs @@ -381,6 +381,20 @@ defmodule ExDoc.Language.ElixirTest do ~s[t() :: String.t()] end + test "skips autolinking if requested" do + ExDoc.Refs.insert([ + {{:module, AutolinkTest.Hidden}, :hidden}, + {{:function, AutolinkTest.Hidden, :foo, 1}, :hidden} + ]) + + assert_skip_autolink_no_warn("AutolinkTest.Hidden") + assert_skip_autolink_no_warn("AutolinkTest.Hidden.foo/1") + end + + defp assert_skip_autolink_no_warn(string) do + assert_unchanged(~m(`#{string}`), skip_code_autolink_to: [string]) + end + test "Elixir basic types" do assert autolink_spec(quote(do: t() :: atom())) == ~s[t() :: atom()] @@ -425,6 +439,16 @@ defmodule ExDoc.Language.ElixirTest do warn(~m"`c:InMemory.unknown/0`") end + test "warning if typespec references filtered module" do + ExDoc.Refs.insert([ + {{:module, AutolinkTest.Keep}, :public}, + {{:function, AutolinkTest.Filtered}, :public}, + {{:type, AutolinkTest.Filtered, :type, 0}, :public} + ]) + + # TODO: testing + end + test "warnings" do ExDoc.Refs.insert([ {{:module, AutolinkTest.Foo}, :public}, diff --git a/test/ex_doc/language/erlang_test.exs b/test/ex_doc/language/erlang_test.exs index 0a986ce37..8a1cc231d 100644 --- a/test/ex_doc/language/erlang_test.exs +++ b/test/ex_doc/language/erlang_test.exs @@ -124,6 +124,21 @@ defmodule ExDoc.Language.ErlangTest do ~s|array:array()| end + test "abstract types - description", c do + assert autolink_doc("{@type myList(X). A special kind of lists ...}", c) == + ~s|myList(X)| + end + + test "abstract types - description+dot", c do + assert autolink_doc("{@type myList(X, Y).}", c) == + ~s|myList(X, Y)| + end + + test "abstract types - no description", c do + assert autolink_doc("{@type myList()}", c) == + ~s|myList()| + end + test "bad module", c do assert ExUnit.CaptureIO.capture_io(:stderr, fn -> assert autolink_doc("{@link bad}", c) == ~s|bad| diff --git a/test/ex_doc/retriever/elixir_test.exs b/test/ex_doc/retriever/elixir_test.exs index 6c69a5636..21b5088f7 100644 --- a/test/ex_doc/retriever/elixir_test.exs +++ b/test/ex_doc/retriever/elixir_test.exs @@ -27,7 +27,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) assert %ExDoc.ModuleNode{ doc_line: 2, @@ -88,7 +88,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) [foo] = mod.docs assert foo.id == "foo/2" @@ -104,7 +104,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) [macro] = mod.docs assert macro.id == "macro/1" @@ -127,7 +127,7 @@ defmodule ExDoc.Retriever.ElixirTest do """) config = %ExDoc.Config{source_url_pattern: "%{path}:%{line}"} - [mod] = Retriever.docs_from_modules([Mod], config) + {[mod], []} = Retriever.docs_from_modules([Mod], config) assert mod.type == :behaviour [callback1, macrocallback1, optional_callback1] = mod.docs @@ -176,7 +176,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [impl] = Retriever.docs_from_modules([Impl], %ExDoc.Config{}) + {[impl], []} = Retriever.docs_from_modules([Impl], %ExDoc.Config{}) [callback1, optional_callback1] = impl.docs assert callback1.id == "callback1/0" @@ -202,7 +202,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) [opaque1, type1] = mod.typespecs assert type1.id == "t:type1/0" @@ -233,7 +233,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod, Mod.Atom], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod, Mod.Atom], %ExDoc.Config{}) assert mod.type == :protocol [foo] = mod.docs @@ -248,7 +248,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([MyStruct], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([MyStruct], %ExDoc.Config{}) [my_struct] = mod.docs assert my_struct.id == "__struct__/0" @@ -263,7 +263,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([MyException], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([MyException], %ExDoc.Config{}) assert mod.title == "MyException" assert mod.type == :exception @@ -287,7 +287,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) [downcase, upcase] = mod.docs assert downcase.id == "downcase/1" @@ -308,7 +308,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Signatures], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Signatures], %ExDoc.Config{}) [remote] = mod.docs assert remote.signature == "remote(options)" @@ -327,7 +327,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mix.Tasks.MyTask], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mix.Tasks.MyTask], %ExDoc.Config{}) assert mod.title == "mix my_task" assert mod.type == :task refute mod.group @@ -369,7 +369,7 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - [mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) overlapping_defaults_2 = Enum.find(mod.docs, &(&1.id == "overlapping_defaults/2")) overlapping_defaults_3 = Enum.find(mod.docs, &(&1.id == "overlapping_defaults/3")) @@ -407,7 +407,8 @@ defmodule ExDoc.Retriever.ElixirTest do end """) - assert [%ExDoc.ModuleNode{} = mod] = Retriever.docs_from_modules([Mod], %ExDoc.Config{}) + assert {[%ExDoc.ModuleNode{} = mod], []} = + Retriever.docs_from_modules([Mod], %ExDoc.Config{}) assert [%ExDoc.TypeNode{id: "t:t/0", annotations: ["since 1.0.0"]}] = mod.typespecs diff --git a/test/ex_doc/retriever/erlang_test.exs b/test/ex_doc/retriever/erlang_test.exs index 64f7ff9d2..4b2dda494 100644 --- a/test/ex_doc/retriever/erlang_test.exs +++ b/test/ex_doc/retriever/erlang_test.exs @@ -24,7 +24,7 @@ defmodule ExDoc.Retriever.ErlangTest do function2() -> ok. """) - [mod] = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) %ExDoc.ModuleNode{ deprecated: nil, @@ -80,7 +80,7 @@ defmodule ExDoc.Retriever.ErlangTest do -module(mod). """) - assert [_] = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) + assert {[_], []} = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) end test "function with no docs is generated", c do @@ -92,7 +92,7 @@ defmodule ExDoc.Retriever.ErlangTest do f() -> ok. """) - [mod] = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([:mod], %ExDoc.Config{}) assert [_] = mod.docs end @@ -111,7 +111,7 @@ defmodule ExDoc.Retriever.ErlangTest do """) config = %ExDoc.Config{source_url_pattern: "%{path}:%{line}"} - [mod] = Retriever.docs_from_modules([:mod], config) + {[mod], []} = Retriever.docs_from_modules([:mod], config) [callback1, optional_callback1] = mod.docs assert callback1.id == "c:callback1/0" @@ -140,7 +140,7 @@ defmodule ExDoc.Retriever.ErlangTest do """) config = %ExDoc.Config{source_url_pattern: "%{path}:%{line}"} - [mod] = Retriever.docs_from_modules([:mod], config) + {[mod], []} = Retriever.docs_from_modules([:mod], config) [opaque1, type1] = mod.typespecs assert opaque1.id == "t:opaque1/0" @@ -166,7 +166,7 @@ defmodule ExDoc.Retriever.ErlangTest do docs: false ) - assert Retriever.docs_from_modules([:no_chunk], %ExDoc.Config{}) == [] + assert {[], []} = Retriever.docs_from_modules([:no_chunk], %ExDoc.Config{}) end # TODO diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs index 802022bba..c138c0e07 100644 --- a/test/ex_doc/retriever_test.exs +++ b/test/ex_doc/retriever_test.exs @@ -12,7 +12,7 @@ defmodule ExDoc.RetrieverTest do end """) - [mod] = Retriever.docs_from_modules([A], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([A], %ExDoc.Config{}) assert mod.doc == nil end @@ -26,7 +26,7 @@ defmodule ExDoc.RetrieverTest do end """) - [mod] = Retriever.docs_from_modules([A], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([A], %ExDoc.Config{}) [foo] = mod.docs assert foo.id == "foo/0" assert foo.annotations == ["since 1.0.0"] @@ -55,7 +55,7 @@ defmodule ExDoc.RetrieverTest do ] } - [qux, bar, foo, baz] = Retriever.docs_from_modules([Foo, Bar, Baz, Qux], config) + {[qux, bar, foo, baz], []} = Retriever.docs_from_modules([Foo, Bar, Baz, Qux], config) assert %{module: Foo, group: :"Group 1"} = foo assert %{module: Bar, group: :"Group 1"} = bar assert %{module: Baz, group: :"Group 2"} = baz @@ -83,7 +83,7 @@ defmodule ExDoc.RetrieverTest do ] } - [mod] = Retriever.docs_from_modules([A], config) + {[mod], []} = Retriever.docs_from_modules([A], config) [bar, baz, foo] = mod.docs assert %{id: "foo/0", group: :"Group 1"} = foo @@ -101,7 +101,7 @@ defmodule ExDoc.RetrieverTest do end """) - [mod] = + {[mod], []} = Retriever.docs_from_modules([A], %ExDoc.Config{ annotations_for_docs: fn metadata -> if metadata[:foo] do @@ -126,7 +126,7 @@ defmodule ExDoc.RetrieverTest do end """) - [mod] = + {[mod], []} = Retriever.docs_from_modules([A], %ExDoc.Config{ annotations_for_docs: fn metadata -> if metadata[:foo] do @@ -157,7 +157,7 @@ defmodule ExDoc.RetrieverTest do end """) - mods = + {mods, []} = Retriever.docs_from_modules( [Nesting.Prefix.B.A, Nesting.Prefix.B.C], %ExDoc.Config{nest_modules_by_prefix: ["Nesting.Prefix.B"]} @@ -171,7 +171,7 @@ defmodule ExDoc.RetrieverTest do assert Enum.at(mods, 1).nested_context == "Nesting.Prefix.B" assert Enum.at(mods, 1).nested_title == ".C" - [mod] = + {[mod], []} = Retriever.docs_from_modules([Nesting.Prefix.B.B.A], %ExDoc.Config{ nest_modules_by_prefix: ["Nesting.Prefix.B.B.A"] }) @@ -201,10 +201,16 @@ defmodule ExDoc.RetrieverTest do ebin_dir = Path.join(c.tmp_dir, "ebin") config = %ExDoc.Config{filter_modules: fn module, _ -> Atom.to_string(module) =~ "A" end} - [a, a_a] = Retriever.docs_from_dir(ebin_dir, config) - assert a.id == "A" - assert a_a.id == "A.A" + assert { + [%{id: "A"}, %{id: "A.A"}], + [%{id: "B"}] + } = Retriever.docs_from_dir(ebin_dir, config) + + assert { + [%{id: "A"}, %{id: "A.A"}], + [%{id: "B"}] + } = Retriever.docs_from_dir([ebin_dir], config) end test "natural sorting", c do @@ -229,7 +235,7 @@ defmodule ExDoc.RetrieverTest do end """) - [mod] = Retriever.docs_from_modules([NaturallySorted], %ExDoc.Config{}) + {[mod], []} = Retriever.docs_from_modules([NaturallySorted], %ExDoc.Config{}) [function_A_0, function_A_1, function_a_0, function_a_1, function_B_0, function_b_0] = mod.docs @@ -255,7 +261,7 @@ defmodule ExDoc.RetrieverTest do end """) - [module_node] = Retriever.docs_from_modules([NoWhitespaceInSignature], %ExDoc.Config{}) + {[module_node], []} = Retriever.docs_from_modules([NoWhitespaceInSignature], %ExDoc.Config{}) %{docs: [%{signature: signature}]} = module_node assert signature == "callback_name(arg1, integer, %Date{}, term, t)" end diff --git a/test/ex_doc_test.exs b/test/ex_doc_test.exs index 85d2da50b..525b60f70 100644 --- a/test/ex_doc_test.exs +++ b/test/ex_doc_test.exs @@ -12,7 +12,7 @@ defmodule ExDocTest do # Simple formatter that returns whatever is passed into it defmodule IdentityFormatter do - def run(modules, config) do + def run(modules, _filtered, config) do {modules, config} end end @@ -63,7 +63,7 @@ defmodule ExDocTest do source_beam: "beam_dir" ] - {{source_dir, _retr_config}, _config} = ExDoc.generate_docs("Elixir", "1", options) + assert {source_dir, _config} = ExDoc.generate_docs("Elixir", "1", options) assert source_dir == options[:source_beam] end diff --git a/test/test_helper.exs b/test/test_helper.exs index bf13c7992..9ce5e3e4b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -22,10 +22,6 @@ defmodule TestHelper do def elixirc(context, filename \\ "nofile", code) do dir = context.tmp_dir - output_dir = context.tmp_dir <> "/html" - File.mkdir_p!(output_dir) - File.write!(output_dir <> "/.ex_doc", "") - src_path = Path.join([dir, filename]) src_path |> Path.dirname() |> File.mkdir_p!() File.write!(src_path, code)