Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/update-git-version-and-manual-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
id: manual-pages
if: steps.commit.outputs.result != '' || inputs.force-rebuild == true
run: |
git add -A external/docs &&
git add -A external/docs static/js/glossary &&
if test true = '${{ inputs.force-rebuild }}' && git diff-index --cached --quiet HEAD --
then
echo '::notice::A manual pages rebuild was requested but resulted in no changes' >&2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-translated-manual-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: |
mkdir -p external/docs/sync &&
git -C '${{ runner.temp }}/git-html-l10n' rev-parse HEAD >external/docs/sync/git-html-l10n.sha &&
git add external/docs/sync/git-html-l10n.sha &&
git add external/docs/sync/git-html-l10n.sha static/js/glossary &&
git add -A external/docs &&
if test true = '${{ inputs.force-rebuild }}' && git diff-index --cached --quiet HEAD --
Expand Down
27 changes: 27 additions & 0 deletions assets/sass/reference.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ h3.plumbing {
@include background-image-2x($baseurl + "images/icons/plumbing-sm", 17px, 14px, 2px 50%);
}

.tooltip {
position: absolute;
/* invisible padding to make it easier to enter with the mouse */
padding: 15px;
visibility: hidden;

.tooltip-content {
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 300px;
font-size: 14px;
line-height: 1.4;
}
}

.tooltip.show {
visibility: visible;
}

.hover-term {
cursor: help;
text-decoration: underline dotted;
}

// § section sign anchor links
#content {
h1>a.anchor,
Expand Down
2 changes: 2 additions & 0 deletions layouts/_default/baseof.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ <h3 hidden="true" data-pagefind-weight="{{ $weight }}">{{ $command_name }}</h3>
</div>
</div>
{{ partialCached "footer.html" . }}
<script src="{{ relURL "js/nanopop.umd.js" }}"></script>
<script src="{{ relURL "js/glossary.js" }}"></script>
</div>
{{ else }}
<div class="inner">
Expand Down
120 changes: 118 additions & 2 deletions script/update-docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "set"
require 'fileutils'
require 'yaml'
require 'json'
require 'diffy'
require_relative "version"
require_relative 'asciidoctor-extensions'
Expand Down Expand Up @@ -95,6 +96,86 @@ def extract_headings(html)
headings
end

def extract_glossary_from_html(content, lang = 'en')
# skip front matter
content = content.split(/^---$/)[2] || content

doc = Nokogiri::HTML::DocumentFragment.parse(content)

glossary = {}

doc.css('dt').each do |dt|
def_anchor = dt.css('a[id^="def_"]').first
next unless def_anchor

term_id = def_anchor['id']
next unless term_id&.start_with?('def_')

term_name = dt.text.strip
# hack to handle this one weird (also) thing
term_names = []
if term_name == 'tree-ish (also treeish)'
term_names = ['tree-ish', 'treeish']
elsif term_name == 'arbre-esque (aussi arbresque)'
term_names = ['arbre-esque', 'arbresque']
else
term_names = [term_name]
end
current_element = dt.next_element
raise 'Expected dd' unless current_element&.name == 'dd'

# Fix up the links because they'regoing to be on a different page
if lang == 'en'
glossary_url = '/docs/gitglossary'
else
glossary_url = "/docs/gitglossary/#{lang}"
end

definition_fragment = Nokogiri::HTML::DocumentFragment.parse(current_element.inner_html.strip)
definition_fragment.css('a[href^="#def_"]').each do |link|
href = link['href']
if href&.start_with?('#def_')
link['href'] = "#{glossary_url}#{href}"
link['target'] = '_blank'
end
end
definition = definition_fragment.to_html

term_names.each do |term|
glossary[term] = definition
end
end

glossary
end

def save_glossary_files(glossary_data_by_lang)
return if glossary_data_by_lang.empty?

glossary_dir = "#{SITE_ROOT}static/js/glossary"
FileUtils.mkdir_p(glossary_dir)

glossary_data_by_lang.each do |lang, glossary_data|
output_file = "#{glossary_dir}/#{lang}.json"
puts " saving glossary data to #{output_file} (#{glossary_data.size} terms)"
File.write(output_file, JSON.pretty_generate(glossary_data))
end
end

def mark_glossary_tooltips(html, glossary_data_by_lang, lang)
current_glossary = glossary_data_by_lang[lang] || {}

html.gsub(/&lt;([^&]+)&gt;/) do |match|
term = $1
# Only mark terms that exist in the glossary
if current_glossary.key?(term)
"<span class=\"hover-term\" data-term=\"#{term}\">&lt;#{term}&gt;</span>"
else
match
end
end
end

def index_l10n_doc(filter_tags, doc_list, get_content)
rebuild = ENV.fetch("REBUILD_DOC", nil)
rerun = ENV["RERUN"] || rebuild || false
Expand Down Expand Up @@ -139,8 +220,15 @@ def index_l10n_doc(filter_tags, doc_list, get_content)
end

check_paths = Set.new([])
glossary_data_by_lang = {}

# Process glossary docs first so that we can use the parsed glossary to mark
# tooltip items in the other documents
glossary_docs = doc_files.select { |entry| File.basename(entry[0], ".#{ext}") == 'gitglossary' }
other_docs = doc_files.reject { |entry| File.basename(entry[0], ".#{ext}") == 'gitglossary' }
ordered_docs = glossary_docs + other_docs

doc_files.each do |entry|
ordered_docs.each do |entry|
full_path, sha = entry
ids = Set.new([])
lang = File.dirname(full_path)
Expand Down Expand Up @@ -177,6 +265,12 @@ def index_l10n_doc(filter_tags, doc_list, get_content)
next if !rerun && lang_data[lang] == asciidoc_sha

html = asciidoc.render

if path == 'gitglossary'
glossary_data_by_lang[lang] = extract_glossary_from_html(html, lang)
puts " extracted #{glossary_data_by_lang[lang].size} glossary terms for #{lang}"
end

html.gsub!(/linkgit:(\S+?)\[(\d+)\]/) do |line|
x = /^linkgit:(\S+?)\[(\d+)\]/.match(line)
relurl = "docs/#{x[1].gsub(/&#x2d;/, '-')}/#{lang}"
Expand Down Expand Up @@ -223,6 +317,8 @@ def index_l10n_doc(filter_tags, doc_list, get_content)
"#{before}{{< relurl \"#{after}\" >}}"
end

html = mark_glossary_tooltips(html, glossary_data_by_lang, lang)

# Write <docname>/<lang>.html
front_matter = {
"category" => "manual",
Expand All @@ -248,6 +344,8 @@ def index_l10n_doc(filter_tags, doc_list, get_content)
lang_data[lang] = asciidoc_sha
end

save_glossary_files(glossary_data_by_lang)

# In some cases, translations are not complete. As a consequence, some
# translated manual pages may point to other translated manual pages that do
# not exist. In these cases, redirect to the English version.
Expand Down Expand Up @@ -432,8 +530,15 @@ def index_doc(filter_tags, doc_list, get_content)
end

check_paths = Set.new([])
glossary_data_by_lang = {}

doc_files.each do |entry|
# Process glossary docs first so that we can use the parsed glossary to mark
# tooltip items in the other documents
glossary_docs = doc_files.select { |entry| File.basename(entry[0].sub(/\.adoc$/, '.txt'), '.txt') == 'gitglossary' }
other_docs = doc_files.reject { |entry| File.basename(entry[0].sub(/\.adoc$/, '.txt'), '.txt') == 'gitglossary' }
ordered_docs = glossary_docs + other_docs

ordered_docs.each do |entry|
path, sha = entry
txt_path = path.sub(/\.adoc$/, '.txt')
ids = Set.new([])
Expand Down Expand Up @@ -482,6 +587,12 @@ def index_doc(filter_tags, doc_list, get_content)

# Generate HTML
html = asciidoc.render

if docname == 'gitglossary'
glossary_data_by_lang['en'] = extract_glossary_from_html(html, 'en')
puts " extracted #{glossary_data_by_lang['en'].size} glossary terms for 'en'"
end

html.gsub!(/linkgit:+(\S+?)\[(\d+)\]/) do |line|
x = /^linkgit:+(\S+?)\[(\d+)\]/.match(line)
if x[1] == "curl"
Expand Down Expand Up @@ -522,6 +633,8 @@ def index_doc(filter_tags, doc_list, get_content)
"#{before}{{< relurl \"#{after}\" >}}"
end

html = mark_glossary_tooltips(html, glossary_data_by_lang, 'en')

doc_versions = version_map.keys.sort{|a, b| Version.version_to_num(a) <=> Version.version_to_num(b)}
doc_version_index = doc_versions.index(version)

Expand Down Expand Up @@ -640,6 +753,9 @@ def index_doc(filter_tags, doc_list, get_content)
end
end
end

save_glossary_files(glossary_data_by_lang)

data["latest-version"] = version if !data["latest-version"] || Version.version_to_num(data["latest-version"]) < Version.version_to_num(version)
end

Expand Down
100 changes: 100 additions & 0 deletions static/js/glossary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
var GitGlossary = {
data: null,
term: null,
tooltip: null,

init: function() {
const language = document.querySelector("html")?.getAttribute("lang") || 'en';
$.getJSON(baseURLPrefix + 'js/glossary/' + language + '.json')
.done((data) => this.onDataLoaded(data));
window.addEventListener('resize', () => this.reposition())
},

onDataLoaded: function(data) {
this.data = data;
const content = document.querySelector('#content');

// Create the popover element
document.body.insertAdjacentHTML('beforeend',
'<div class="tooltip"><div class="tooltip-content"></div></div>'
);
this.tooltip = document.body.lastElementChild;
this.attachHoverEvents(content);
},

show: function() {
this.tooltip.classList.add('show');
this.reposition();
},

hide: function() {
this.tooltip.classList.remove('show');
},

reposition: function() {
const result = NanoPop.reposition(this.term, this.tooltip, {
position: 'bottom',
margin: -7,
container: {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth
}
});
},

attachHoverEvents: function(content) {
let timeout = undefined;

content.addEventListener('mouseover', (e) => {
if (e.target.classList.contains('hover-term')) {
console.log(this.term);
this.term = e.target;
const term = e.target.dataset.term;
const definition = this.data[term] || '';
const truncatedDefinition = this.truncateWords(definition, 60);

const language = document.querySelector("html")?.getAttribute("lang") || 'en';
const glossaryUrl = language === 'en' ? '/docs/gitglossary' : `/docs/gitglossary/${language}`;

this.tooltip.querySelector('.tooltip-content').innerHTML = `
<a href="${glossaryUrl}#def_${term}" target="_blank">
<strong>&lt;${term}&gt;</strong>
</a>
<br><br>
${truncatedDefinition}
`;
this.show();
}
});

content.addEventListener('mouseout', (e) => {
if (e.target.classList.contains('hover-term')) {
this.hide();
}
});

// Keep popover open when hovering over it
this.tooltip.addEventListener('mouseenter', () => {
this.show();
});

this.tooltip.addEventListener('mouseleave', () => {
this.hide();
});
},

truncateWords: function(text, maxWords) {
const words = text.split(/\s+/);
if (words.length <= maxWords) {
return text;
}
return words.slice(0, maxWords).join(' ') + '...';
},
};

// Initialize when document is ready
$(document).ready(() => {
GitGlossary.init();
});
3 changes: 3 additions & 0 deletions static/js/nanopop.umd.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading