SmartBlocks icon indicating copy to clipboard operation
SmartBlocks copied to clipboard

Loading Bible verses

Open jonathanvoelkle opened this issue 4 years ago • 4 comments

✂️ Copy of your #42SmartBlock from Roam

  • BibleEmbed #42SmartBlock

    • :hiccup [span.bible-embed "<%INPUT:select bible verse like Rom 8 37-39%>"]
  • BibleLink #42SmartBlock

    • :hiccup [span.bible-link "<%INPUT:select bible verse like Rom 8 37-39%>"]

📋 Describe the SmartBlock

Replaces bible references with their corresponing verses.

✅ Describe any prerequisites or dependencies that are required for this SmartBlock

This block uses a lot of js which I placed externally in [[roam/js]], please let me know if something is not working or you have questions

You need to place two code blocks, one being the list of abbrevations under a #42Settting (this is basically a csv stored in a variable) and one under {{[[roam/js]]}}, then you can add the SmartBlock, which is just a fast way to insert the hiccup.

Please be aware that this currently uses my self-build API, so for extensive use, please use something different or contact me.

    - #42Setting bibleabbr
gn;gn;Genesis;1Moses;1Mo
ex;ex;Exodus;2Moses;2Mo
lv;lv;Leviticus;3Moses;3Mo
nm;nm;Numbers;4Moses;4Mo
dt;dt;Deuteronomy;5Moses;5Mo
js;js;Joshua;Josua;Jos
jud;jud;Judges;Richter;Ri
rt;rt;Ruth;Rut
1sm;1sm;1Samuel;1Sam
2sm;2sm;2Samuel;2Sam
1kgs;1kgs;1Kings;1Könige;1Koenige;1Konige;1Kö;1Koe;1Ko
2kgs;2kgs;2Kings;2Könige;2Koenige;2Konige;2Kö;2Koe;2Ko
1ch;1ch;1Chronicles;1Chr
2ch;2ch;2Chronicles;2Chr
ezr;ezr;Ezra;Esra;Esr
ne;ne;Nehemiah;Neh
et;et;Esther;Ester;Est
job;job;Job;Hiob;Hi
ps;ps;Psalms;Psalm;Psalmen
prv;prv;Proverbs;Sprüche;Sprueche;Spruche;Spr
ec;ec;Ecclesiastes;Prediger;Pred;Ecc
so;so;Song of Solomon;Hl;Hohe Lied
is;is;Isaiah;Jesaja;Jes;Jesaia
jr;jr;Jeremiah;Jeremia;Jer
lm;lm;Lamentations;Klagelieder;Kla
ez;ez;Ezekiel;Hesekiel;Ezechiel;Hes
dn;dn;Daniel;Dan
ho;ho;Hosea;Hos
jl;jl;Joel;Joe
am;am;Amos
ob;ob;Obadiah;Obadja
jn;jn;Jonah;Jona;Jon
mi;mi;Micah;Micha;Mi
na;na;Nahum;Nah
hk;hk;Habakkuk;Habakuk;Hab
zp;zp;Zephaniah;Zefanja;Zefania;Zef
hg;hg;Haggai;Hag
zc;zc;Zechariah;Sacharja;Sacharia;Sach
ml;ml;Malachi;Maleachi;Mal
mt;mt;Matthew;Matthäus;Matthaeus;Matthaus;Mat;Matt
mk;mk;Mark;Markus
lk;lk;Luke;Lukas
jo;jo;John;Johannes;Joh
act;act;Acts;Apostelgeschichte;Apg
rm;rm;Romans;Römer;Roemer;Romer;Röm;Roem;Rom
1co;1co;1Corinthians;1Cor;1Korinther;1Korinter;1Kor
2co;2co;2Corinthians;2Cor;2Korinther;2Korinter;2Kor
gl;gl;Galatians;Galater;Gal
eph;eph;Ephesians;Epheser;Eph
ph;ph;Philippians;Philipper;Phil
cl;cl;Colossians;Kolosser;Kol;Co;Col
1ts;1ts;1Thessalonians;1Thessalonicher;1Thes
2ts;2ts;2Thessalonians;2Thessalonicher;2Thes
1tm;1tm;1Timothy;1Timotheus;1Tim
2tm;2tm;2Timothy;2Timotheus;2Tim
tt;tt;Titus;Tit
phm;phm;Philemon;Phim
hb;hb;Hebrews;Hebräer;Hebraeer;Hebraer;Hebr;Heb
jm;jm;James;Jakobus;Jak
1pe;1pe;1pet;1Peter;1Petrus;1Petr
2pe;2pe;2pet;2Peter;2Petrus;2Petr
1jo;1jo;1joh;1John;1Johannes
2jo;2jo;2joh;2John;2Johannes
3jo;3jo;3joh;3John;3Johannes
jd;jd;Jude;Judas;Jud
re;re;rev;Revelation;Offenbarung;Offb
  • {{[[roam/js]]}}


function display_error (err) {
  console.log(err)
}

window.display_error = display_error;

// add toggle for sidebar
const sidebarPress = new KeyboardEvent("keydown", {
    bubbles: true, cancelable: true, keyCode: 220, ctrlKey: true, shiftKey: true
});
  
const sidebarPressMac = new KeyboardEvent("keydown", {
  bubbles: true, cancelable: true, keyCode: 220, metaKey: true, shiftKey: true
});
  
function createButton() {
  if (!document.getElementById('sideBarDiv')) {
    console.log("sidebar div does not exist")
    var spanOne = document.createElement('span');
    spanOne.classList.add('bp3-popover-wrapper');
    var spanTwo = document.createElement('span');
    spanTwo.classList.add('bp3-popover-target');
    spanOne.appendChild(spanTwo);
    var toggleSideBar = document.createElement('span');
    toggleSideBar.id = 'sideBarDiv';
    toggleSideBar.classList.add('bp3-icon-menu-open', 'bp3-button', 'bp3-minimal');
    spanTwo.appendChild(toggleSideBar);
    var roamTopbar = document.getElementsByClassName("roam-topbar");
    roamTopbar[0].childNodes[0].appendChild(spanOne);
    toggleSideBar.onclick = toggleTheSideBar;
  }
}

function toggleTheSideBar() {
  document.activeElement.dispatchEvent(sidebarPress);
  document.activeElement.dispatchEvent(sidebarPressMac);
}

setTimeout(createButton, 200);

function create_number_abbrev(verses) {
  const sorted_verses = [...new Set(verses)].sort((a,b) => a - b)
  let return_string = "" 
  
  for(let i = 0; i < sorted_verses.length; i++) {
    if(sorted_verses[i] !== sorted_verses[i+1] - 1) {
	  // gap to next
      return_string += sorted_verses[i] + ","
    } else {
      if(sorted_verses[i] == sorted_verses[i-1] + 1) {
        // no gap
        return_string += "-"  
      } else {
        // gap to previous
        return_string += sorted_verses[i] + "-"
      }  
    }
  }
  
  return return_string.split("-").filter(x => x).join("-").slice(0, -1); 
}

window.create_number_abbrev = create_number_abbrev;

// global method to open bible verse in sidebar 
// using data from localstorage 
// param: slug
function open_sidebar_with_verse(slug) {
  const [b, c, v] = slug.split("_")
  const v_highlight = v.split(",").map(x => parseInt(x))
  const { verses, name, chap } = JSON.parse(localStorage.getItem(`bible_${b}_${c}`))
  console.log(verses, v_highlight)
  // const slug = slg + Math.random()
  
  const verses_html = verses.map((v, i) => `<span class=\"verse-number\"
	>${i + 1}</span><p class=\"${v_highlight.includes(i + 1) ? 'h-v': ''}\">${v.replace("'", "\'")}</p><span class=\"separator\"> * </span>`).join(``);
  
  
  const sidebar_content_inner = `
    <div
      style="
        margin-right: 8px;
        margin-left: 8px;
        padding-left: 16px;
        padding-right: 16px;
      "
    >
      <div
        style="align-items: flex-start; margin-left: -16px; margin-right: -16px"
        draggable="true"
        class="flex-h-box window-headers"
      >
        <span style="margin-right: 2px" class="bp3-icon bp3-minimal"
          ><span
            id="${slug}-caret"
            class="bp3-icon-standard bp3-icon-caret-down rm-caret rm-caret-open rm-caret-showing"
            onClick="window.toggle_chapter_in_sidebar('${slug}')"
          ></span
        ></span>
        <div><span>Outline of: <span class="b-hidden-if-open rm-block-ref">${name} ${chap}:${create_number_abbrev(v_highlight)}<span></span></div>
        <div style="flex: 1 1 0px"></div>
        <span class="bp3-button bp3-minimal bp3-icon-cross bp3-small" 
          onClick="window.remove_chapter_from_sidebar('${slug}')"
        ></span>
      </div>
      <div style="position: relative; padding-bottom: 8px">
        <div class="rm-sidebar-outline">
          <div style="width: 100%">
            <h1 tabindex="-1" class="rm-title-display"><span>${name} ${chap}</span></h1>
          </div>
          <div class="rm-block-children rm-block__children rm-level-0">
            <div
              data-page-links="[]"
              data-path-page-links="[]"
              class="roam-block-container rm-block rm-not-focused block-bullet-view"
            >
              <div class="rm-block-main rm-block__self">
                <div
                  id=""
                  class="rm-block__input rm-block__input--view roam-block dont-unfocus-block hoverparent rm-block-text"
                  tabindex="0"
                ><blockquote class="rm-bq rm-left-border"><span class="bible" id="${slug}-bible">${verses_html}</span></blockquote></div>
                <div class="rm-block-separator"></div>
              </div>
              <div class="rm-block-children rm-block__children rm-level-1"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  `

  
  if(!document.getElementById('roam-right-sidebar-content')) {
    console.log("sidebar is not opened")
    toggleTheSideBar();
    setTimeout(() => { 
      const side_bar = document.getElementById('roam-right-sidebar-content').firstChild
      const side_bar_append = document.createElement("div");
      side_bar_append.setAttribute("id", slug)
      side_bar_append.innerHTML = sidebar_content_inner;
      side_bar.prepend(side_bar_append);
    }, 200);
  } else {
    console.log("sidebar is opened")
    const side_bar = document.getElementById('roam-right-sidebar-content').firstChild
    const side_bar_append = document.createElement("div");
    side_bar_append.setAttribute("id", slug)
    side_bar_append.innerHTML = sidebar_content_inner;
    side_bar.prepend(side_bar_append);
  }
}

window.open_sidebar_with_verse = open_sidebar_with_verse;


function toggle_chapter_in_sidebar(slug) {
  document.getElementById(slug).firstElementChild.classList.toggle("minimized-embed");
  document.getElementById(slug+"-caret").classList.toggle("rm-caret-closed")
  document.getElementById(slug+"-caret").classList.toggle("rm-caret-open")
}

window.toggle_chapter_in_sidebar = toggle_chapter_in_sidebar;



function remove_chapter_from_sidebar(slug) {
  document.getElementById(slug).remove()
  const side_bar = document.getElementsByClassName("sidebar-content")[0]
  if(side_bar && side_bar.children.length == 0) {
    toggleTheSideBar()
  }
}

window.remove_chapter_from_sidebar = remove_chapter_from_sidebar;

const parser_regex = /(d?\w+)[\s]?(\d{1,3})[^\d]+([\d\-\,\s]*)/gi

async function bibleAbbrevations(b) {
  if(roam42 && roam42.settings) {
  	const file_of_abbr = await roam42.settings.get("bibleabbr");
  	list_of_abbr = file_of_abbr.split("\n")
    list_of_abbr.pop();
    list_of_abbr.shift();
    
    const abbr_map = new Map();
    list_of_abbr.forEach(b_abbr => {
      const opts = b_abbr.split(";").map(x => x.trim())
      const main_abbr = opts.shift();
      opts.forEach(o => abbr_map.set(o.toLowerCase(), main_abbr))
    })
    
    const book = abbr_map.get(b.toLowerCase())
    
    return book
  } else {
    display_error("roam42 does not exist")
  }
}
  
async function bibleVerseParser(bvc) {
  const create_list = (b, e) => {
  	console.log(b, e)
  	return Array.from(Array(e - b + 1).keys()).map((x) => x + b)
  }
  
  const get_verse_numbers_from_string = (v) => v.trim()
    .split(',')
    .map((p) => {
      let [b, e] = p.split('-')
      return e ? create_list(parseInt(b), parseInt(e)) : parseInt(b)
    })
    .flat()
  
  const [, b, c, vs] = [...bvc.trim().matchAll(parser_regex)][0];
  
  const book = await bibleAbbrevations(b)
  
  console.log(book)
  
  const res = {
    book,
    chapter: parseInt(c),
    verses: get_verse_numbers_from_string(vs)
  }
  console.log(res)
  return res
}

window.bibleVerseParser = bibleVerseParser;

const local_storage_prefix = "bible"
const base_url = 'https://bible-server.jonathanvoelkle.workers.dev'

async function getBibleVerseAsync(book, chapter, verses) {
  console.log("get async ", book, chapter, verses)
  
  let local_query = localStorage.getItem(`${local_storage_prefix}_${book}_${chapter}`)
  if(local_query) {
    const local_json = JSON.parse(local_query)
    console.log("local", local_json)
    
    // was previously queryed with error
    if(local_json.error) {
      return {
        error: true,
        errorMessage: local_json.errorMessage
      }
    } 
    
    // was previously queried with ok
    else {
      return {
        ...local_json, 
        verses: local_json.verses
        	.map((v, i) => ({
              ver: v, 
              number: i + 1, 
            }))
        	.filter((_, i) => verses.includes(i + 1))
      }
    } 
  } else {
    const res = await fetch(`${base_url}/${book}/${chapter}`)
    if(res.ok) {
  	  const res_json = await res.json();  
      localStorage.setItem(`${local_storage_prefix}_${book}_${chapter}`, 
                           JSON.stringify(res_json));
      console.log("fetched", res_json)
      return {
        ...res_json, 
        verses: res_json.verses
        	.map((v, i) => ({
              ver: v, 
              number: i + 1, 
            }))
        	.filter((_, i) => verses.includes(i + 1))
      }
    } else {
      // TODO
      console.log(res)
    }
  }
}

window.getBibleVerseAsync = getBibleVerseAsync


const b_embed_re = /\(\((([el]):b\|(\w{1,4}):(\d{1,3}):(.+))\)\)/gi;
const b_embed_re_inner = /(([el]):b\|(\w{1,4}):(\d{1,3}):(.+))/gi;

const general_class = "bible"
const embed_class = `${general_class}-embed`
const link_class = `${general_class}-link`

function createSlug(b, c, v) {
  return `${b}_${c}_${v.map(x => x.number).join(",")}`
}

async function transform_bible_block(block, method) {
  const query = block.innerText;
  console.log(query)

  // remove trigger class such that it is not queried again
  block.classList.toggle(`${general_class}-${method}`);
  
  const bq = await bibleVerseParser(query);
  console.log("bg", bq)
  const { book, chapter, verses } = await bq;
  const bible_resp = await getBibleVerseAsync(book, chapter, verses)
  
  console.log(bible_resp)
  
  if(bible_resp && !bible_resp.error) {
    block.innerHTML = create_block(method, bible_resp)
  } else {
    block.classList.toggle("bible-error");
  }
  console.log("hello", bible_resp)
}

function create_embed_block(data) {
  console.log("data", data)
  const { verses, name, chap, book } = data;
  
  const vers_abbr = create_number_abbrev(verses.map(x => x.number))
  const vers_html = verses.map(({number, ver}) => `<sup>${number}</sup> ${ver}`).join(" ");
  
  const slug = createSlug(book, chap, verses)
  console.log(slug, name, chap)
  
  return `<blockquote class="bp3-blockquote custom-embed">${vers_html}<span class="bp3-popover-wrapper bible-verse-link-wrapper"
    > —&nbsp;<span class="bp3-popover-target" 
		onClick='window.open_sidebar_with_verse("${slug}")'      
	  ><span class="rm-block-ref dont-focus-block"
        ><span>${name} ${chap}:${vers_abbr}</span></span
      ></span
    ></span
  ></blockquote>`
}

function create_link_block(data) {
  const { verses, name, chap, book } = data 
  
  const vers_abbr = create_number_abbrev(verses.map(x => x.number))
  
  const slug = createSlug(book, chap, verses)
  console.log(slug, name, chap)
  
  return `<span class="bp3-popover-wrapper bible-verse-link-wrapper"
    ><span class="bp3-popover-target"
        onClick='window.open_sidebar_with_verse("${slug}")' 
      ><span class="rm-block-ref dont-focus-block"
        ><span>${name} ${chap}:${vers_abbr}</span></span
      ></span
    ></span
  >`
}

function create_block(method, bible_data) {
  if(method === "embed") {
    return create_embed_block(bible_data)
  }
  if(method === "link") {
   	return create_link_block(bible_data) 
  }
  return "error: method not allowed"
}


function transform_bible(){ 
  let bible_embed = document.getElementsByClassName(embed_class);
  let bible_link = document.getElementsByClassName(link_class);

  for(let block of bible_embed) {
    transform_bible_block(block, "embed")
  }
  for(let block of bible_link) {
    transform_bible_block(block, "link")
  }
}

// setup
setTimeout(transform_bible, 200);
const checker = setInterval(transform_bible, 1000);

setTimeout(function() {
  // debug mode:
  // clearInterval(checker);
}, 12000)

📷 Screenshot of your #42SmartBlock workflow/template from Roam

Screenshot_2020-12-28 Roam Research – A note taking tool for networked thought

Screenshot_2020-12-28 Roam Research – A note taking tool for networked thought (1)

💡 Additional Info

https://www.loom.com/share/ef8aedbd8c3c44eab28c249bcf419a5e

Result looks like this: Screenshot_2021-01-02 Roam Research – A note taking tool for networked thought

jonathanvoelkle avatar Dec 28 '20 21:12 jonathanvoelkle

Jonathan, beautiful work. This inspires me for my own Bible tools. thank you for sharing and your hard work.

TfTHacker avatar Dec 28 '20 21:12 TfTHacker

Just realized I forgot the css

.bible > .verse-number {
  width: 2em;
  left: -2rem;
  top: auto;
  position: absolute;
  text-align: end;
  // line-height: 2rem;
  // display: inline-block;
  font-size: .8em;
  // font-weight: 600;
}

.bible > p {
  display: inline;
}

.bible > .h-v {
  text-decoration: underline;
}

.bible > .separator {
  	font-feature-settings: 'case';
	-webkit-font-feature-settings: 'case';
	-ms-font-feature-settings: 'case';
	-moz-font-feature-settings: 'case';
}

.bible > .separator:last-of-type {
  display: none;
}

.b-hidden-if-open {
  display: none;
}

.minimized-embed .b-hidden-if-open {
  display: revert;
}

.minimized-embed .rm-sidebar-outline {
  display: none;
}

.minimized-embed > div:last-child {
  padding-bottom: 0px !important;
}

.sidebar-content > div > div {
  border-bottom: 1px solid rgb(138, 155, 168);
}

.sidebar-content > div:last-of-type  > div:last-child {
  border-bottom: none;
}

.bible-verse-link-wrapper {
  white-space: nowrap;
}

jonathanvoelkle avatar Dec 28 '20 22:12 jonathanvoelkle

Could you add a screen shot of what these look like installed on a roam page? Also, where does it pull the bible passages from?

Meh-S-Eze avatar Jan 01 '21 17:01 Meh-S-Eze

@MehsEzeerf I've updated the issue with a screenshot of the result (with my personal [[roam/css]] theme) the bible passages are pulled from https://bible-server.jonathanvoelkle.workers.dev/ps/110 simple api I build myself, probably you can do the same with some static json files. The translation is currently German. Feel free to reach out if you need help setting this up or something similar (either here, on twitter or on the slack channel)

jonathanvoelkle avatar Jan 02 '21 10:01 jonathanvoelkle