diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c0cc4413c..bc40858e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ * ![Enhancement][badge-enhancement] Documenter now support Azure DevOps Repos URL scheme when generating edit and source links pointing to the repository. ([#1462][github-1462], [#1463][github-1463], [#1471][github-1471]) +* ![Enhancement][badge-enhancement] Code blocks now support copy to clipboard button to quickly run the code snippet in Julia REPL. ([#1055](github-1055), [#1454](github-1454)) + ## Version `v0.25.3` * ![Feature][badge-feature] Documenter can now deploy from GitLab CI to GitHub Pages with `Documenter.GitLab`. ([#1448][github-1448]) @@ -586,6 +588,7 @@ [github-1046]: https://github.com/JuliaDocs/Documenter.jl/issues/1046 [github-1047]: https://github.com/JuliaDocs/Documenter.jl/pull/1047 [github-1054]: https://github.com/JuliaDocs/Documenter.jl/pull/1054 +[github-1055]: https://github.com/JuliaDocs/Documenter.jl/issues/1055 [github-1057]: https://github.com/JuliaDocs/Documenter.jl/issues/1057 [github-1061]: https://github.com/JuliaDocs/Documenter.jl/pull/1061 [github-1062]: https://github.com/JuliaDocs/Documenter.jl/pull/1062 @@ -679,6 +682,7 @@ [github-1448]: https://github.com/JuliaDocs/Documenter.jl/pull/1448 [github-1440]: https://github.com/JuliaDocs/Documenter.jl/pull/1440 [github-1452]: https://github.com/JuliaDocs/Documenter.jl/pull/1452 +[github-1454]: https://github.com/JuliaDocs/Documenter.jl/pull/1454 [github-1462]: https://github.com/JuliaDocs/Documenter.jl/issues/1462 [github-1463]: https://github.com/JuliaDocs/Documenter.jl/pull/1463 [github-1468]: https://github.com/JuliaDocs/Documenter.jl/pull/1468 diff --git a/assets/html/js/clipboard.js b/assets/html/js/clipboard.js new file mode 100644 index 0000000000..c5a1d548f7 --- /dev/null +++ b/assets/html/js/clipboard.js @@ -0,0 +1,26 @@ +// libraries: jquery, clipboard +// arguments: $, ClipboardJS + +// Copies code block to clipboard. +$(document).ready(function() { + var clipboard = new ClipboardJS('.copy-button'); + var btns = document.querySelectorAll('.copy-button'); + clipboard.on('success', function(e) { + showTooltip(e.trigger, 'Copied!'); + }) + + for (var i = 0; i < btns.length; i++) { + btns[i].addEventListener('mouseleave', clearTooltip); + btns[i].addEventListener('blur', clearTooltip); + } + + function clearTooltip(e) { + e.currentTarget.setAttribute('class', 'copy-button button'); + e.currentTarget.removeAttribute('aria-label'); + } + + function showTooltip(elem, msg) { + elem.setAttribute('class', 'copy-button button tooltipped tooltipped-s'); + elem.setAttribute('aria-label', msg); + } +}); diff --git a/assets/html/scss/documenter/components/_all.scss b/assets/html/scss/documenter/components/_all.scss index 8a71bf3fc7..45e1c0b79c 100644 --- a/assets/html/scss/documenter/components/_all.scss +++ b/assets/html/scss/documenter/components/_all.scss @@ -1,3 +1,4 @@ @import "admonition"; @import "docstring"; @import "example"; +@import "clipboard"; diff --git a/assets/html/scss/documenter/components/_clipboard.scss b/assets/html/scss/documenter/components/_clipboard.scss new file mode 100644 index 0000000000..5a0f0f6b6a --- /dev/null +++ b/assets/html/scss/documenter/components/_clipboard.scss @@ -0,0 +1,68 @@ +.tooltipped { + position: relative; +} + +.tooltipped:after { + position: absolute; + display: none; + padding: 5px 8px; + color: #fff; + text-align: center; + content: attr(aria-label); + background: rgba(0, 0, 0, .8); + border-radius: 3px; +} + +.tooltipped:before { + position: absolute; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, .8); + content: ""; + border: 5px solid transparent; +} + +.tooltipped:hover:before, +.tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; +} + +.tooltipped-s:after { + top: 100%; + right: 50%; + margin-top: 5px; + transform: translateX(50%); +} + +.tooltipped-s:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, .8); +} + +.snippet { + position:relative; + overflow:visible; + } + +.snippet .copy-button { + transition: opacity .3s ease-in-out; + opacity: 0; + font-size: 0.9em; + position: absolute; + right: 4px; + top: 4px; +} + +.snippet:hover .copy-button, +.snippet .copy-button:focus { + opacity: 1; +} diff --git a/assets/html/themes/documenter-dark.css b/assets/html/themes/documenter-dark.css index ea7d8034f1..8db1641809 100644 --- a/assets/html/themes/documenter-dark.css +++ b/assets/html/themes/documenter-dark.css @@ -7279,6 +7279,57 @@ html.theme--documenter-dark { opacity: 1; } html.theme--documenter-dark .documenter-example-output { background-color: #1f2424; } + html.theme--documenter-dark .tooltipped { + position: relative; } + html.theme--documenter-dark .tooltipped:after { + position: absolute; + display: none; + padding: 5px 8px; + color: #fff; + text-align: center; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; } + html.theme--documenter-dark .tooltipped:before { + position: absolute; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + content: ""; + border: 5px solid transparent; } + html.theme--documenter-dark .tooltipped:hover:before, + html.theme--documenter-dark .tooltipped:hover:after, + html.theme--documenter-dark .tooltipped:active:before, + html.theme--documenter-dark .tooltipped:active:after, + html.theme--documenter-dark .tooltipped:focus:before, + html.theme--documenter-dark .tooltipped:focus:after { + display: inline-block; + text-decoration: none; } + html.theme--documenter-dark .tooltipped-s:after { + top: 100%; + right: 50%; + margin-top: 5px; + transform: translateX(50%); } + html.theme--documenter-dark .tooltipped-s:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); } + html.theme--documenter-dark .snippet { + position: relative; + overflow: visible; } + html.theme--documenter-dark .snippet .copy-button { + transition: opacity .3s ease-in-out; + opacity: 0; + font-size: 0.9em; + position: absolute; + right: 4px; + top: 4px; } + html.theme--documenter-dark .snippet:hover .copy-button, + html.theme--documenter-dark .snippet .copy-button:focus { + opacity: 1; } html.theme--documenter-dark .content pre { border: 1px solid #5e6d6f; } html.theme--documenter-dark .content code { diff --git a/assets/html/themes/documenter-light.css b/assets/html/themes/documenter-light.css index b5dbe628e2..1eafe3fa7d 100644 --- a/assets/html/themes/documenter-light.css +++ b/assets/html/themes/documenter-light.css @@ -7242,6 +7242,66 @@ h1:hover .docs-heading-anchor-permalink, h2:hover .docs-heading-anchor-permalink .documenter-example-output { background-color: white; } +.tooltipped { + position: relative; } + +.tooltipped:after { + position: absolute; + display: none; + padding: 5px 8px; + color: #fff; + text-align: center; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; } + +.tooltipped:before { + position: absolute; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + content: ""; + border: 5px solid transparent; } + +.tooltipped:hover:before, +.tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; } + +.tooltipped-s:after { + top: 100%; + right: 50%; + margin-top: 5px; + transform: translateX(50%); } + +.tooltipped-s:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); } + +.snippet { + position: relative; + overflow: visible; } + +.snippet .copy-button { + transition: opacity .3s ease-in-out; + opacity: 0; + font-size: 0.9em; + position: absolute; + right: 4px; + top: 4px; } + +.snippet:hover .copy-button, +.snippet .copy-button:focus { + opacity: 1; } + .content pre { border: 1px solid #dbdbdb; } diff --git a/src/Utilities/DOM.jl b/src/Utilities/DOM.jl index d26cfd10cf..ca7e0aba82 100644 --- a/src/Utilities/DOM.jl +++ b/src/Utilities/DOM.jl @@ -248,7 +248,7 @@ function Base.show(io::IO, n::Node) print(io, '<', n.name) for (name, value) in n.attributes print(io, ' ', name) - isempty(value) || print(io, '=', repr(escapehtml(value))) + isempty(value) || print(io, '=', repr(escapehtml(value, escape_newlines=true))) end if n.name in VOID_ELEMENTS print(io, "/>") @@ -281,7 +281,7 @@ When no escaping is needed then the same object is returned, otherwise a new string is constructed with the characters escaped. The returned object should always be treated as an immutable copy and compared using `==` rather than `===`. """ -function escapehtml(text::AbstractString) +function escapehtml(text::AbstractString; escape_newlines=false) if occursin(r"[<>&'\"]", text) buffer = IOBuffer() for char in text @@ -291,7 +291,11 @@ function escapehtml(text::AbstractString) char === '\'' ? write(buffer, "'") : char === '"' ? write(buffer, """) : write(buffer, char) end - String(take!(buffer)) + escaped_text = String(take!(buffer)) + if escape_newlines + return replace(escaped_text, "\n"=>" ") + end + escaped_text else text end diff --git a/src/Writers/HTMLWriter.jl b/src/Writers/HTMLWriter.jl index c46e776615..85ab16995c 100644 --- a/src/Writers/HTMLWriter.jl +++ b/src/Writers/HTMLWriter.jl @@ -449,6 +449,7 @@ module RD const jqueryui = RemoteLibrary("jqueryui", "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js") const lunr = RemoteLibrary("lunr", "https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.6/lunr.min.js") const lodash = RemoteLibrary("lodash", "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js") + const clipboardjs = RemoteLibrary("clipboard", "https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.6/clipboard.min.js") # headroom const headroom_version = "0.10.3" @@ -638,7 +639,7 @@ function render(doc::Documents.Document, settings::HTML=HTML()) @warn "not creating 'documenter.js', provided by the user." else r = JSDependencies.RequireJS([ - RD.jquery, RD.jqueryui, RD.headroom, RD.headroom_jquery, + RD.jquery, RD.jqueryui, RD.headroom, RD.headroom_jquery, RD.clipboardjs ]) RD.mathengine!(r, settings.mathengine) RD.highlightjs!(r, settings.highlights) @@ -1609,10 +1610,15 @@ mdconvert(b::Markdown.BlockQuote, parent; kwargs...) = Tag(:blockquote)(mdconver mdconvert(b::Markdown.Bold, parent; kwargs...) = Tag(:strong)(mdconvert(b.text, parent; kwargs...)) function mdconvert(c::Markdown.Code, parent::MDBlockContext; kwargs...) - @tags pre code + @tags pre code button span i language = Utilities.codelang(c.language) language = isempty(language) ? "none" : language - pre(code[".language-$(language)"](c.code)) + copy_icon = span[".icon"](i[".fas .fa-copy"]) + code_block = [ + button[".copy-button .button", :title=>"Copy to clipboard", Symbol("data-clipboard-text")=>c.code](copy_icon), + code[".language-$(language)"](c.code) + ] + pre[".snippet"](code_block) end mdconvert(c::Markdown.Code, parent; kwargs...) = Tag(:code)(c.code)