markdownr 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f53bb26ac9275a03286e9cfaf0adf08c46582ecf1822fac465beab8cf5aa0106
4
- data.tar.gz: 45b246a4f1ce159590f87c16317218cb30fd4a65a5e2fb87bf923e8f7882f91b
3
+ metadata.gz: e9a74b1e181f0fd5361c73e64a6e00659451cf075e1e41ec4f45c0cee9167e9d
4
+ data.tar.gz: 44b5d07121159a9efadac6ee85bd4528d2490ec419b0752dbe15e1336e94eea2
5
5
  SHA512:
6
- metadata.gz: 741c4405596d671907e5c74faa2db43ebb32c748054bdd2bec345aff4b6a92c86a38c04e972e7a19476cd32c774e94955b4fca5e221ee179f55cd44321cae052
7
- data.tar.gz: 6a446e37812f760c922113f754d4a8271e81052a9f5b9aed2f8bcae22e3d39a06cfa4adba41e7611f72a64219ecdf189fe33d9cda6a08e848d4b4d5417715a7d
6
+ metadata.gz: 866416e07423170e5ec3c84e60da99682bfabb07b09cf1eb74f29e7e74f509c9982ec292641721ec0d054f5361bd6012fc00a34755ab46987c4462e63569926e
7
+ data.tar.gz: 1a18c15a57094d2a5d72670d760d82d1eeca80ab1c694cc550a7ff69e352c6dd9e889840e515b0a40c9e59a9b48e9aa85744d6983f37f33c572b90cdd39c28a8
@@ -137,10 +137,12 @@ module MarkdownServer
137
137
  label = display || heading_text
138
138
  %(<a class="wiki-link" href="##{h(anchor)}">#{h(label)}</a>)
139
139
  else
140
- resolved = resolve_wiki_link(target)
140
+ file_part, anchor_part = target.split("#", 2)
141
+ anchor_suffix = anchor_part ? "##{anchor_part.downcase.gsub(/\s+/, '-').gsub(/[^\w-]/, '')}" : ""
142
+ resolved = resolve_wiki_link(file_part)
141
143
  label = display || target
142
144
  if resolved
143
- %(<a class="wiki-link" href="/browse/#{encode_path_component(resolved).gsub('%2F', '/')}">#{h(label)}</a>)
145
+ %(<a class="wiki-link" href="/browse/#{encode_path_component(resolved).gsub('%2F', '/')}#{anchor_suffix}">#{h(label)}</a>)
144
146
  else
145
147
  %(<span class="wiki-link broken">#{h(label)}</span>)
146
148
  end
@@ -227,6 +229,7 @@ module MarkdownServer
227
229
  str.scan(/\[\[([^\]]+)\]\]/) do |match|
228
230
  raw = match[0]
229
231
  m_start = $~.begin(0)
232
+ m_end = $~.end(0)
230
233
  result += h(str[last_end...m_start])
231
234
  target, display = raw.include?("|") ? raw.split("|", 2) : [raw, nil]
232
235
  label = display || target
@@ -234,14 +237,16 @@ module MarkdownServer
234
237
  anchor = target[1..].downcase.gsub(/\s+/, "-").gsub(/[^\w-]/, "")
235
238
  result += %(<a class="wiki-link" href="##{h(anchor)}">#{h(label)}</a>)
236
239
  else
237
- resolved = resolve_wiki_link(target)
240
+ file_part, anchor_part = target.split("#", 2)
241
+ anchor_suffix = anchor_part ? "##{anchor_part.downcase.gsub(/\s+/, '-').gsub(/[^\w-]/, '')}" : ""
242
+ resolved = resolve_wiki_link(file_part)
238
243
  if resolved
239
- result += %(<a class="wiki-link" href="/browse/#{encode_path_component(resolved).gsub('%2F', '/')}">#{h(label)}</a>)
244
+ result += %(<a class="wiki-link" href="/browse/#{encode_path_component(resolved).gsub('%2F', '/')}#{anchor_suffix}">#{h(label)}</a>)
240
245
  else
241
246
  result += %(<span class="wiki-link broken">#{h(label)}</span>)
242
247
  end
243
248
  end
244
- last_end = $~.end(0)
249
+ last_end = m_end
245
250
  end
246
251
  result += h(str[last_end..])
247
252
  result
@@ -252,7 +257,7 @@ module MarkdownServer
252
257
  when Array
253
258
  value.map { |v|
254
259
  str = v.to_s
255
- if str =~ /\A\[\[([^\]]+)\]\]\z/
260
+ if str.include?("[[")
256
261
  render_inline_wiki_links(str)
257
262
  else
258
263
  %(<span class="tag">#{h(str)}</span>)
@@ -575,6 +580,133 @@ module MarkdownServer
575
580
  out.length > 10_000 ? out[0, 10_000] : out
576
581
  end
577
582
 
583
+ def blueletterbible_html(html, url)
584
+ base = "https://www.blueletterbible.org"
585
+
586
+ # ── Word ──────────────────────────────────────────────────────────────
587
+ word = html[/<h6[^>]+class="lexTitle(?:Gk|Hb)"[^>]*>(.*?)<\/h6>/im, 1]
588
+ &.gsub(/<[^>]+>/, "")&.strip || ""
589
+
590
+ # ── Transliteration ───────────────────────────────────────────────────
591
+ transliteration = html[/<div[^>]+id="lexTrans".*?<em>(.*?)<\/em>/im, 1]&.strip || ""
592
+
593
+ # ── Pronunciation + audio ─────────────────────────────────────────────
594
+ pronunciation = html[/class="[^"]*lexicon-pronunc[^"]*"[^>]*>\s*([^\n<]{1,50})/i, 1]&.strip || ""
595
+ data_pronunc = html[/data-pronunc="([a-fA-F0-9]{20,})"/i, 1] || ""
596
+ audio_btn = if data_pronunc.length > 10
597
+ au = "#{base}/lang/lexicon/lexPronouncePlayer.cfm?skin=#{data_pronunc}"
598
+ %(<button onclick="var a=this._a||(this._a=new Audio('#{h(au)}'));a.currentTime=0;a.play();" ) +
599
+ %(style="background:none;border:none;cursor:pointer;padding:0 0 0 4px;font-size:1.1em;vertical-align:middle;" title="Play pronunciation">&#128266;</button>)
600
+ else
601
+ ""
602
+ end
603
+
604
+ # ── Part of speech ────────────────────────────────────────────────────
605
+ pos = html[/<div[^>]+id="lexPart".*?small-text-right"[^>]*>(.*?)<\/div>/im, 1]
606
+ &.gsub(/<[^>]+>/, "")&.strip || ""
607
+
608
+ # ── Info table ────────────────────────────────────────────────────────
609
+ info_rows = [
610
+ ["Word", h(word)],
611
+ ["Transliteration", "<em>#{h(transliteration)}</em>"],
612
+ ["Pronunciation", "#{h(pronunciation)}#{audio_btn}"],
613
+ ["Part of Speech", h(pos)],
614
+ ]
615
+ info_html = %(<table class="blb-table">) +
616
+ info_rows.map { |label, v|
617
+ %(<tr><th class="blb-th">#{h(label)}</th><td>#{v}</td></tr>)
618
+ }.join + "</table>"
619
+
620
+ # ── Inflections ───────────────────────────────────────────────────────
621
+ infl_html = ""
622
+ if (m = html.match(/<div\s[^>]*id="greek-tr-inflections"[^>]*>/im))
623
+ after_infl = html[m.end(0)..]
624
+ stop = after_infl.index(/<div\s[^>]*id="greek-(?:mgnt|lxx)-inflections"/i) || after_infl.length
625
+ infl_section = after_infl[0...stop]
626
+ inflections = []
627
+ infl_section.scan(/<div\s[^>]*class="greekInflection"[^>]*>(.*?)<\/div>\s*<\/div>/im) do |mv|
628
+ chunk = mv[0]
629
+ href = chunk[/href="([^"]+)"/i, 1]
630
+ gk = chunk[/<span[^>]+class="Gk"[^>]*>(.*?)<\/span>/im, 1]&.gsub(/<[^>]+>/, "")&.strip || ""
631
+ freq = chunk[/&#8212;\s*(\d+)x<\/a>/i, 1]&.to_i || 0
632
+ next if gk.empty? || freq.zero?
633
+ inflections << { word: gk, freq: freq,
634
+ href: href ? base + href.gsub("&amp;", "&") : nil }
635
+ end
636
+ inflections.sort_by! { |i| -i[:freq] }
637
+ if inflections.any?
638
+ rows = inflections.map { |i|
639
+ link = i[:href] ? %(<a href="#{h(i[:href])}" target="_blank" rel="noopener">#{h(i[:word])}</a>) : h(i[:word])
640
+ %(<tr><td>#{link}</td><td class="blb-right">#{i[:freq]}x</td></tr>)
641
+ }.join
642
+ infl_html = %(<h4 class="blb-heading">Inflections</h4>) +
643
+ %(<table class="blb-table"><thead><tr><th class="blb-th">Form</th>) +
644
+ %(<th class="blb-th blb-right">Count</th></tr></thead><tbody>#{rows}</tbody></table>)
645
+ end
646
+ end
647
+
648
+ # ── Biblical Usage ────────────────────────────────────────────────────
649
+ usage_html = ""
650
+ if (um = html.match(/<div[^>]+id="outlineBiblical"[^>]*>/im))
651
+ after_usage = html[um.end(0)..]
652
+ if (inner = after_usage.match(/\A\s*<div>([\s\S]*?)<\/div>\s*<\/div>/im))
653
+ cleaned = inner[1]
654
+ .gsub(/<(\/?)(\w+)[^>]*>/) { "<#{$1}#{$2.downcase}>" }
655
+ .gsub(/[ \t]+/, " ")
656
+ .strip
657
+ usage_html = %(<h4 class="blb-heading">Biblical Usage</h4><div class="blb-usage">#{cleaned}</div>)
658
+ end
659
+ end
660
+
661
+ # ── Concordance ───────────────────────────────────────────────────────
662
+ conc_html = ""
663
+ trans_name = html[/id="bibleTable"[^>]+data-translation="([^"]+)"/i, 1] || ""
664
+ verses = []
665
+ html.split(/<div\s[^>]*id="bVerse_\d+"[^>]*>/).drop(1).each do |chunk|
666
+ cite_href = chunk[/tablet-order-2[^>]*>[\s\S]{0,400}?href="([^"]+)"/im, 1] || ""
667
+ cite = chunk[/tablet-order-2[^>]*>[\s\S]{0,400}?<a[^>]*>(.*?)<\/a>/im, 1]
668
+ &.gsub(/<[^>]+>/, "")&.strip || ""
669
+
670
+ # Process verse HTML: highlight the matched word, strip all Strong's refs
671
+ raw_html = chunk[/class="EngBibleText[^"]*"[^>]*>([\s\S]*?)<\/div>/im, 1] || ""
672
+ raw_html.gsub!(/<img[^>]*>/, "")
673
+ raw_html.gsub!(/<a[^>]*class="hide-for-tablet"[^>]*>[\s\S]*?<\/a>/im, "")
674
+ raw_html.gsub!(/<span[^>]*class="hide-for-tablet"[^>]*>[\s\S]*?<\/span>/im, "")
675
+ verse_html = raw_html.gsub(/<span\s[^>]*class="word-phrase"[^>]*>([\s\S]*?)<\/span>/im) do
676
+ inner = $1
677
+ word = inner.sub(/<sup[\s\S]*/im, "").gsub(/<[^>]+>/, "")
678
+ .gsub(/&nbsp;/i, " ").strip
679
+ inner.match?(/<sup[^>]*class="[^"]*strongs criteria[^"]*"/i) ?
680
+ %(<span class="blb-match">#{h(word)}</span>) : h(word)
681
+ end
682
+ verse_html.gsub!(/<sup[^>]*>[\s\S]*?<\/sup>/im, "")
683
+ verse_html.gsub!(/<[^>]+>/, "")
684
+ verse_html.gsub!(/&nbsp;/i, " ")
685
+ verse_html.gsub!(/&#(\d+);/) { [$1.to_i].pack("U") rescue " " }
686
+ verse_html.gsub!(/&#x([\da-f]+);/i) { [$1.to_i(16)].pack("U") rescue " " }
687
+ verse_html.gsub!(/&amp;/, "&").gsub!(/&lt;/, "<").gsub!(/&gt;/, ">")
688
+ verse_html.gsub!(/\s+/, " ")
689
+ verse_html.strip!
690
+ # Strip the mobile citation prefix ("Mat 5:17 - ") left by hide-for-tablet removal
691
+ verse_html.sub!(/\A#{Regexp.escape(cite)}\s*-\s*/i, "")
692
+
693
+ next if cite.empty? || verse_html.empty?
694
+ full_href = cite_href.empty? ? nil : (cite_href.start_with?("http") ? cite_href : base + cite_href)
695
+ verses << { cite: cite, verse_html: verse_html, href: full_href }
696
+ end
697
+ if verses.any?
698
+ heading = trans_name.empty? ? "Concordance" : "Concordance (#{h(trans_name)})"
699
+ rows = verses.map { |v|
700
+ link = v[:href] ? %(<a href="#{h(v[:href])}" target="_blank" rel="noopener">#{h(v[:cite])}</a>) : h(v[:cite])
701
+ %(<tr><td class="blb-nowrap">#{link}</td><td>#{v[:verse_html]}</td></tr>)
702
+ }.join
703
+ conc_html = %(<h4 class="blb-heading">#{heading}</h4>) +
704
+ %(<table class="blb-table"><tbody>#{rows}</tbody></table>)
705
+ end
706
+
707
+ info_html + infl_html + usage_html + conc_html
708
+ end
709
+
578
710
  def compile_regexes(query)
579
711
  words = query.split(/\s+/).reject(&:empty?)
580
712
  return nil if words.empty?
@@ -675,8 +807,15 @@ module MarkdownServer
675
807
  html = fetch_external_page(url)
676
808
  halt 502, '{"error":"fetch failed"}' unless html
677
809
 
678
- title = page_title(html).sub(/ [-–] .*/, "").strip
679
- JSON.dump({ title: title, html: page_html(html, url) })
810
+ if url.match?(/blueletterbible\.org\/lexicon\//i)
811
+ raw = page_title(html)
812
+ title = raw.match(/^([GH]\d+ - \w+)/i)&.[](1)&.sub(" - ", " – ") ||
813
+ raw.sub(/ [-–] .*/, "").strip
814
+ JSON.dump({ title: title, html: blueletterbible_html(html, url) })
815
+ else
816
+ title = page_title(html).sub(/ [-–] .*/, "").strip
817
+ JSON.dump({ title: title, html: page_html(html, url) })
818
+ end
680
819
  end
681
820
 
682
821
  get "/search/?*" do
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.5"
2
+ VERSION = "0.5.7"
3
3
  end
data/views/layout.erb CHANGED
@@ -909,6 +909,22 @@
909
909
  .link-preview-popup th, .link-preview-popup td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
910
910
  .link-ctx-popup-body th, .link-preview-popup th { background: #f5f0e4; }
911
911
 
912
+ /* Blue Letter Bible popup tables */
913
+ .blb-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-bottom: 0.6rem; }
914
+ .blb-table th, .blb-table td { padding: 3px 7px; border: 1px solid #ddd; }
915
+ .blb-th { text-align: left; font-weight: normal; background: #f5f0e4; color: #555; width: 38%; }
916
+ .blb-right { text-align: right; }
917
+ .blb-nowrap { white-space: nowrap; vertical-align: top; }
918
+ .blb-match { color: #b33; font-weight: 600; }
919
+ .blb-heading { font-size: 0.82rem; font-weight: 600; margin: 0.7rem 0 0.25rem; color: #555; text-transform: uppercase; letter-spacing: 0.04em; }
920
+ .blb-usage { font-size: 0.85rem; }
921
+ .blb-usage ol { margin: 0.1rem 0 0.1rem 1.3rem; padding: 0; list-style-type: decimal; }
922
+ .blb-usage ol ol { list-style-type: lower-alpha; }
923
+ .blb-usage ol ol ol { list-style-type: lower-roman; }
924
+ .blb-usage ol ol ol ol { list-style-type: lower-alpha; }
925
+ .blb-usage li { margin-bottom: 0.15rem; }
926
+ .blb-usage p { margin: 0; }
927
+
912
928
  /* Footnote tooltips */
913
929
  .footnote-tooltip {
914
930
  position: absolute;
@@ -1704,6 +1720,26 @@
1704
1720
  historyStack = [];
1705
1721
  }
1706
1722
 
1723
+ function applyPopupAnchor(hash) {
1724
+ if (!hash || !popup) return;
1725
+ var id = hash.replace(/^#/, '');
1726
+ try {
1727
+ var target = popup.querySelector('[id="' + id.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]');
1728
+ if (!target) return;
1729
+ // Scroll the popup container (which has overflow-y:auto) to the target,
1730
+ // accounting for the sticky header so the heading lands just below it
1731
+ var header = popup.querySelector('.link-ctx-popup-header');
1732
+ var headerHeight = header ? header.offsetHeight : 0;
1733
+ var targetTop = target.getBoundingClientRect().top;
1734
+ var popupTop = popup.getBoundingClientRect().top;
1735
+ popup.scrollTop += targetTop - popupTop - headerHeight - 8;
1736
+ // Append section heading text to the popup title
1737
+ if (/^H[1-6]$/.test(target.tagName)) {
1738
+ var titleEl = popup.querySelector('.link-ctx-popup-title span');
1739
+ if (titleEl) titleEl.textContent = titleEl.textContent + ' \u203a ' + target.textContent;
1740
+ }
1741
+ } catch(e) {}
1742
+ }
1707
1743
 
1708
1744
  function handleLink(anchor, x, y, chained) {
1709
1745
  if (!chained) historyStack = [];
@@ -1712,12 +1748,14 @@
1712
1748
  var label = anchor.textContent.trim() || href;
1713
1749
  // For chained popup navigation keep the current link rect; for new popups measure the anchor
1714
1750
  var linkRect = chained ? currentPopupPos.linkRect : anchor.getBoundingClientRect();
1751
+ var linkHash = (function() { try { return new URL(href, location.href).hash; } catch(e) { return ''; } })();
1715
1752
 
1716
1753
  if (isLocalMd(href)) {
1717
1754
  var path = previewPath(href);
1718
1755
  var cached = cache[path];
1719
1756
  if (cached && typeof cached === 'object') {
1720
1757
  showPopup(x, y, cached.title || label, cached.html, href, linkRect);
1758
+ applyPopupAnchor(linkHash);
1721
1759
  } else if (cached === false) {
1722
1760
  showPopup(x, y, label,
1723
1761
  '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
@@ -1733,6 +1771,7 @@
1733
1771
  var bodyHtml = data.html + (data.frontmatter_html || '');
1734
1772
  cache[path] = { title: data.title, html: bodyHtml };
1735
1773
  updatePopup(bodyHtml, data.title || label);
1774
+ applyPopupAnchor(linkHash);
1736
1775
  })
1737
1776
  .catch(function() {
1738
1777
  cache[path] = false;
@@ -1786,7 +1825,7 @@
1786
1825
  if (!anchor) { hidePopup(); return; }
1787
1826
  var href = anchor.getAttribute('href');
1788
1827
  if (!href || isAnchorOnly(href)) { hidePopup(); return; }
1789
- if (!anchor.closest('.md-content')) { hidePopup(); return; }
1828
+ if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
1790
1829
  e.preventDefault();
1791
1830
  handleLink(anchor, e.clientX, e.clientY);
1792
1831
  });
@@ -1808,7 +1847,7 @@
1808
1847
  if (!anchor) { hidePopup(); return; }
1809
1848
  var href = anchor.getAttribute('href');
1810
1849
  if (!href || isAnchorOnly(href)) { hidePopup(); return; }
1811
- if (!anchor.closest('.md-content')) { hidePopup(); return; }
1850
+ if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
1812
1851
  e.preventDefault();
1813
1852
  var touch = e.changedTouches[0];
1814
1853
  handleLink(anchor, touch.clientX, touch.clientY);
@@ -1821,24 +1860,26 @@
1821
1860
  <% if settings.link_tooltips %>
1822
1861
  // Hover — same popup as click, triggered after a short delay when no popup is active
1823
1862
  (function() {
1824
- var content = document.querySelector('.md-content');
1825
- if (!content) return;
1826
1863
  var hoverTimer = null;
1827
- content.querySelectorAll('a').forEach(function(a) {
1828
- var href = a.getAttribute('href');
1829
- if (!href || isAnchorOnly(href)) return;
1830
- if (!isLocalMd(href) && !isExternal(href)) return;
1831
- a.addEventListener('mouseenter', function(e) {
1832
- clearTimeout(hoverTimer);
1833
- if (popup) return;
1834
- var x = e.clientX, y = e.clientY;
1835
- hoverTimer = setTimeout(function() {
1836
- if (!popup) handleLink(a, x, y, false);
1837
- }, 300);
1838
- });
1839
- a.addEventListener('mouseleave', function() {
1840
- clearTimeout(hoverTimer);
1841
- if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
1864
+ var containers = document.querySelectorAll('.md-content, .frontmatter');
1865
+ if (!containers.length) return;
1866
+ containers.forEach(function(container) {
1867
+ container.querySelectorAll('a').forEach(function(a) {
1868
+ var href = a.getAttribute('href');
1869
+ if (!href || isAnchorOnly(href)) return;
1870
+ if (!isLocalMd(href) && !isExternal(href)) return;
1871
+ a.addEventListener('mouseenter', function(e) {
1872
+ clearTimeout(hoverTimer);
1873
+ if (popup) return;
1874
+ var x = e.clientX, y = e.clientY;
1875
+ hoverTimer = setTimeout(function() {
1876
+ if (!popup) handleLink(a, x, y, false);
1877
+ }, 300);
1878
+ });
1879
+ a.addEventListener('mouseleave', function() {
1880
+ clearTimeout(hoverTimer);
1881
+ if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
1882
+ });
1842
1883
  });
1843
1884
  });
1844
1885
  })();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdownr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn