Skip to content

mikejed/Rock-Key-Translator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rock Key Translator

A small browser tool for Rock RMS administrators that translates a Rock entity Key (its IdHash) to its integer Id, and back again. Select a value on any page of your Rock instance, trigger the tool, and the translated value is copied to your clipboard and shown in a quick popup.

It works while you are logged into Rock: the request is authenticated by your existing session cookie, so there is no API key to configure and no secret stored anywhere.

What it does

  1. Reads the text you have selected (or prompts you for a value).
  2. Sends it to Rock's Lava RenderTemplate API on the site you are currently on.
  3. Translates a Key to an Id (the common case), or an Id to a Key.
  4. Copies the result to your clipboard and shows it in an alert.

The default direction is Key to Id; the reverse is also supported.

Installation

Browser extension (recommended)

Supports Chrome, Edge, and Firefox. Safari is not supported.

Chrome / Edge

  1. Go to chrome://extensions (or edge://extensions).
  2. Enable Developer mode.
  3. Click Load unpacked and select the extension/ folder.

Firefox

  1. In the extension/ folder, use manifest.firefox.json as the manifest (rename it to manifest.json for a local load, or build a separate Firefox package).
  2. Go to about:debugging > This Firefox > Load Temporary Add-on and select the manifest. (A temporary add-on is removed on restart; a permanent install requires AMO signing.)

After loading, trigger it three ways: the toolbar button, a right-click Translate Rock Key / Id on selected text, or the keyboard shortcut (Alt+Shift+K by default).

Bookmarklet (no install required)

Prefer not to install the extension? The bookmarklet has the same behavior. Nothing to build or run; it is ready to copy.

  1. Open bookmarklet/bookmarklet.url.txt and copy its entire contents (or copy from the box below).
  2. Create a new bookmark on your bookmarks bar.
  3. Paste the copied text as the bookmark's URL and save.
  4. On any Rock page, select a Key or Id (including inside a modal) and click the bookmark. With nothing selected, it prompts for a value.
Bookmarklet URL (click to expand, then copy)
javascript:(function()%7B%0A%2F%2F%20Rock%20Key%20Translator%3A%20canonical%20shared%20core.%0A%2F%2F%0A%2F%2F%20This%20single%20function%20implements%20the%20whole%20behavior%20(Sections%205-10%20of%0A%2F%2F%20REQUIREMENTS.md)%3A%20obtain%20the%20input%2C%20compose%20the%20Lava%20template%2C%20call%20the%0A%2F%2F%20RenderTemplate%20API%2C%20parse%20the%20response%2C%20copy%20the%20result%2C%20and%20report%20it.%0A%2F%2F%0A%2F%2F%20It%20is%20intentionally%20SELF-CONTAINED%3A%20it%20references%20nothing%20in%20module%20scope%2C%20so%0A%2F%2F%20it%20can%20be%20(a)%20injected%20verbatim%20by%20the%20extension%20via%0A%2F%2F%20chrome.scripting.executeScript(%7B%20func%3A%20rockKeyTranslate%20%7D)%20and%20(b)%20inlined%0A%2F%2F%20into%20the%20bookmarklet.%20Keep%20all%20helpers%20as%20inner%20functions.%0A%2F%2F%0A%2F%2F%20%60preselectedText%60%20is%20the%20text%20supplied%20by%20a%20trigger%20that%20already%20knows%20the%0A%2F%2F%20selection%20(the%20extension's%20context-menu%20click%20passes%20info.selectionText).%0A%2F%2F%20When%20it%20is%20null%2Fempty%2C%20the%20core%20reads%20the%20page%20selection%20itself%2C%20then%20prompts.%0A%0Afunction%20rockKeyTranslate(preselectedText)%20%7B%0A%20%20const%20ENDPOINT%20%3D%20%22%2Fapi%2FLava%2FRenderTemplate%22%3B%0A%0A%20%20%2F%2F%20---%20template%20---------------------------------------------------------%0A%0A%20%20%2F%2F%20Escape%20the%20user%20value%20so%20it%20cannot%20break%20out%20of%20the%20Lava%20string%20literal%20or%0A%20%20%2F%2F%20inject%20Lava%20control%20sequences%20(it%20runs%20server-side%20as%20the%20logged-in%20user).%0A%20%20function%20escapeForLava(value)%20%7B%0A%20%20%20%20return%20String(value)%0A%20%20%20%20%20%20.replace(%2F'%2Fg%2C%20%22%5C%5C'%22)%0A%20%20%20%20%20%20.replace(%2F%5C%7B%5C%7B%7C%5C%7D%5C%7D%7C%5C%7B%25%7C%25%5C%7D%2Fg%2C%20%22%22)%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20The%20default%20Key%20%3C-%3E%20Id%20translator.%20The%20input%20is%20injected%20as%20a%20quoted%20string%0A%20%20%2F%2F%20literal.%20FromIdHash%20returns%20the%20same%20integer%20when%20given%20an%20Id%2C%20so%0A%20%20%2F%2F%20%60valId%20%3D%3D%20input%60%20detects%20%22input%20was%20an%20Id%22%20(encode%20it)%20versus%20%22input%20was%20a%0A%20%20%2F%2F%20Key%22%20(decode%20it).%20Validated%20against%20Rock's%20RenderTemplate%20endpoint.%0A%20%20function%20buildTemplate(input)%20%7B%0A%20%20%20%20const%20v%20%3D%20escapeForLava(input)%3B%0A%20%20%20%20return%20%5B%0A%20%20%20%20%20%20%22%7B%25%20assign%20input%20%3D%20'%22%20%2B%20v%20%2B%20%22'%20%7C%20Trim%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20assign%20return%20%3D%20''%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20if%20input%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C1%20%7C%20AddToDictionary%3A'ErrMsg'%2C'No%20value%20was%20provided%20to%20translate.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20assign%20valId%20%3D%20input%20%7C%20FromIdHash%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20if%20valId%20%3D%3D%20input%20%25%7D%20%2F%2F-%20input%20is%20an%20Id%3B%20encode%20it%20to%20a%20Key.%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20assign%20asInt%20%3D%20input%20%7C%20AsInteger%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20if%20asInt%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C2%20%7C%20AddToDictionary%3A'ErrMsg'%2C'That%20value%20is%20not%20a%20valid%20Key%20or%20Id.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20key%20%3D%20asInt%20%7C%20ToIdHash%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20if%20key%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C3%20%7C%20AddToDictionary%3A'ErrMsg'%2C'The%20value%20could%20not%20be%20translated.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C0%20%7C%20AddToDictionary%3A'Msg'%2C'The%20Key%20for%20that%20Id%20is%20'%20%7C%20AddToDictionary%3A'Val'%2Ckey%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20if%20valId%20%3D%3D%20empty%20or%20valId%20%3D%3D%20null%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C4%20%7C%20AddToDictionary%3A'ErrMsg'%2C'The%20value%20could%20not%20be%20translated.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C0%20%7C%20AddToDictionary%3A'Msg'%2C'That%20Key%20translates%20to%20the%20Id%20'%20%7C%20AddToDictionary%3A'Val'%2CvalId%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%7B%20return%20%7C%20ToJSON%20%7D%7D%22%0A%20%20%20%20%5D.join(%22%5Cn%22)%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20input%20acquisition%20------------------------------------------------%0A%0A%20%20%2F%2F%20Read%20the%20selection%20from%20a%20single%20window%3A%20its%20active%20form%20field%20(whose%20text%0A%20%20%2F%2F%20window.getSelection()%20does%20not%20always%20cover)%20or%20its%20document%20selection.%0A%20%20%2F%2F%20Wrapped%20in%20try%2Fcatch%20because%20cross-origin%20frames%20are%20not%20readable.%0A%20%20function%20selectionFromWindow(win)%20%7B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20const%20el%20%3D%20win.document.activeElement%3B%0A%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20el%20%26%26%0A%20%20%20%20%20%20%20%20(el.tagName%20%3D%3D%3D%20%22INPUT%22%20%7C%7C%20el.tagName%20%3D%3D%3D%20%22TEXTAREA%22)%20%26%26%0A%20%20%20%20%20%20%20%20typeof%20el.selectionStart%20%3D%3D%3D%20%22number%22%20%26%26%0A%20%20%20%20%20%20%20%20el.selectionStart%20!%3D%3D%20el.selectionEnd%0A%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20return%20el.value.substring(el.selectionStart%2C%20el.selectionEnd)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20win.getSelection%20%3F%20win.getSelection().toString()%20%3A%20%22%22%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20return%20%22%22%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20Read%20the%20current%20document%20first%2C%20then%20same-origin%20child%20frames.%20Rock%20renders%0A%20%20%2F%2F%20modal%20dialogs%20inside%20%23modal-popup_iframe%2C%20so%20a%20value%20highlighted%20in%20a%20modal%0A%20%20%2F%2F%20lives%20in%20a%20child%20frame%2C%20not%20the%20top%20document.%20The%20toolbar%2Fkeyboard%20triggers%0A%20%20%2F%2F%20inject%20into%20the%20top%20frame%20only%2C%20so%20without%20this%20they%20would%20miss%20it.%0A%20%20function%20readSelection()%20%7B%0A%20%20%20%20let%20s%20%3D%20selectionFromWindow(window)%3B%0A%20%20%20%20if%20(s%20%26%26%20s.trim())%20return%20s%3B%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20window.frames.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20s%20%3D%20selectionFromWindow(window.frames%5Bi%5D)%3B%0A%20%20%20%20%20%20if%20(s%20%26%26%20s.trim())%20return%20s%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20%22%22%3B%0A%20%20%7D%0A%0A%20%20function%20resolveInput()%20%7B%0A%20%20%20%20const%20raw%20%3D%20(preselectedText%20!%3D%20null%20%26%26%20String(preselectedText)%20!%3D%3D%20%22%22)%0A%20%20%20%20%20%20%3F%20String(preselectedText)%0A%20%20%20%20%20%20%3A%20readSelection()%3B%0A%20%20%20%20const%20value%20%3D%20(raw%20%7C%7C%20%22%22).trim()%3B%0A%0A%20%20%20%20%2F%2F%20A%20valid%20selection%20is%20non-empty%20with%20no%20internal%20whitespace.%0A%20%20%20%20if%20(value%20%26%26%20!%2F%5Cs%2F.test(value))%20%7B%0A%20%20%20%20%20%20return%20value%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Otherwise%20prompt.%20Cancel%20or%20empty%20aborts%20silently.%0A%20%20%20%20const%20entered%20%3D%20window.prompt(%22Enter%20a%20Rock%20Key%20(IdHash)%20or%20Id%20to%20translate%3A%22)%3B%0A%20%20%20%20if%20(entered%20%3D%3D%20null)%20return%20null%3B%0A%20%20%20%20const%20trimmed%20%3D%20entered.trim()%3B%0A%20%20%20%20return%20trimmed%20%3F%20trimmed%20%3A%20null%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20clipboard%20--------------------------------------------------------%0A%0A%20%20async%20function%20copyToClipboard(text)%20%7B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20if%20(navigator.clipboard%20%26%26%20navigator.clipboard.writeText)%20%7B%0A%20%20%20%20%20%20%20%20await%20navigator.clipboard.writeText(text)%3B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%2F%2F%20fall%20through%20to%20the%20legacy%20path%0A%20%20%20%20%7D%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20const%20ta%20%3D%20document.createElement(%22textarea%22)%3B%0A%20%20%20%20%20%20ta.value%20%3D%20text%3B%0A%20%20%20%20%20%20ta.setAttribute(%22readonly%22%2C%20%22%22)%3B%0A%20%20%20%20%20%20ta.style.position%20%3D%20%22fixed%22%3B%0A%20%20%20%20%20%20ta.style.top%20%3D%20%22-1000px%22%3B%0A%20%20%20%20%20%20ta.style.opacity%20%3D%20%220%22%3B%0A%20%20%20%20%20%20document.body.appendChild(ta)%3B%0A%20%20%20%20%20%20ta.focus()%3B%0A%20%20%20%20%20%20ta.select()%3B%0A%20%20%20%20%20%20const%20ok%20%3D%20document.execCommand(%22copy%22)%3B%0A%20%20%20%20%20%20document.body.removeChild(ta)%3B%0A%20%20%20%20%20%20return%20ok%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20HTTP%20status%20messages%20--------------------------------------------%0A%0A%20%20function%20messageForStatus(status%2C%20statusText)%20%7B%0A%20%20%20%20switch%20(status)%20%7B%0A%20%20%20%20%20%20case%20401%3A%0A%20%20%20%20%20%20%20%20return%20%22Not%20Authorized.%20Your%20Rock%20session%20may%20have%20expired%3B%20please%20log%20in%20again.%22%3B%0A%20%20%20%20%20%20case%20403%3A%0A%20%20%20%20%20%20%20%20return%20%22Forbidden.%20Your%20account%20does%20not%20have%20permission%20to%20render%20Lava%20via%20the%20API.%22%3B%0A%20%20%20%20%20%20case%20404%3A%0A%20%20%20%20%20%20%20%20return%20%22Endpoint%20not%20found.%20%22%20%2B%20ENDPOINT%20%2B%20%22%20is%20unavailable%20on%20this%20server.%22%3B%0A%20%20%20%20%20%20case%20405%3A%0A%20%20%20%20%20%20%20%20return%20%22Method%20Not%20Allowed.%20The%20endpoint%20did%20not%20accept%20the%20request%20method.%22%3B%0A%20%20%20%20%20%20case%20429%3A%0A%20%20%20%20%20%20%20%20return%20%22Too%20Many%20Requests.%20Please%20wait%20a%20moment%20and%20try%20again.%22%3B%0A%20%20%20%20%20%20default%3A%0A%20%20%20%20%20%20%20%20if%20(status%20%3E%3D%20500%20%26%26%20status%20%3C%3D%20599)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20%22Rock%20encountered%20a%20server%20error%20while%20rendering%20the%20template.%22%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20%22Unexpected%20response%20from%20Rock%20(HTTP%20%22%20%2B%20status%20%2B%0A%20%20%20%20%20%20%20%20%20%20(statusText%20%3F%20%22%20%22%20%2B%20statusText%20%3A%20%22%22)%20%2B%20%22).%22%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20main%20flow%20--------------------------------------------------------%0A%0A%20%20return%20(async%20function%20run()%20%7B%0A%20%20%20%20const%20input%20%3D%20resolveInput()%3B%0A%20%20%20%20if%20(input%20%3D%3D%20null)%20return%3B%20%2F%2F%20aborted%20silently%0A%0A%20%20%20%20let%20res%3B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20res%20%3D%20await%20fetch(ENDPOINT%2C%20%7B%0A%20%20%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%20%20%20%20credentials%3A%20%22same-origin%22%2C%0A%20%20%20%20%20%20%20%20headers%3A%20%7B%20%22Content-Type%22%3A%20%22application%2Fx-www-form-urlencoded%3B%20charset%3DUTF-8%22%20%7D%2C%0A%20%20%20%20%20%20%20%20body%3A%20buildTemplate(input)%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20window.alert(%22Could%20not%20reach%20Rock.%20Check%20your%20connection%20and%20that%20you%20are%20still%20logged%20in.%22)%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(res.status%20!%3D%3D%20200)%20%7B%0A%20%20%20%20%20%20window.alert(messageForStatus(res.status%2C%20res.statusText))%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20raw%20%3D%20await%20res.text()%3B%0A%20%20%20%20let%20data%3B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20let%20parsed%20%3D%20JSON.parse(raw)%3B%0A%20%20%20%20%20%20if%20(typeof%20parsed%20%3D%3D%3D%20%22string%22)%20parsed%20%3D%20JSON.parse(parsed)%3B%20%2F%2F%20double-decode%0A%20%20%20%20%20%20data%20%3D%20parsed%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20window.alert(%22Received%20an%20unexpected%20response%20from%20Rock.%5Cn%5Cn%22%20%2B%20raw.slice(0%2C%20300))%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(data%20%26%26%20data.ErrCode%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20const%20val%20%3D%20String(data.Val)%3B%0A%20%20%20%20%20%20const%20copied%20%3D%20await%20copyToClipboard(val)%3B%0A%20%20%20%20%20%20const%20tail%20%3D%20copied%0A%20%20%20%20%20%20%20%20%3F%20%22%5Cn%5Cn(Copied%20to%20your%20clipboard.)%22%0A%20%20%20%20%20%20%20%20%3A%20%22%5Cn%5Cn(Could%20not%20copy%20to%20the%20clipboard%3B%20copy%20it%20manually%20from%20above.)%22%3B%0A%20%20%20%20%20%20window.alert((data.Msg%20%7C%7C%20%22%22)%20%2B%20val%20%2B%20tail)%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20window.alert(data%20%26%26%20data.ErrMsg%20%3F%20data.ErrMsg%20%3A%20%22The%20value%20could%20not%20be%20translated.%22)%3B%0A%20%20%20%20%7D%0A%20%20%7D)()%3B%0A%7D%0A%0ArockKeyTranslate()%3B%0A%7D)()%3B

Maintainers only: the bookmarklet is generated from the core (extension/core.js). See the bookmarklet readme file for details on regenerating this after making changes to the core.

Usage

  1. Make sure you are logged into your Rock instance.
  2. Select a Key (IdHash) or an Id on the page. Alternatively, trigger the tool with nothing selected and enter the value when prompted.
  3. Trigger the tool:
  • Click the bookmarklet if you used that method.
  • Or if you have the extension installed, you can click the extension toolbar button, use the right-click menu, or use the keyboard shortcut (Alt+Shift+K by default).
  1. The translated value is copied to your clipboard, and an alert shows the result and confirms whether the copy succeeded.

Notes

  • Requires an HTTPS Rock page and a valid login session.
  • The account you are signed in as must be permitted to use the /api/Lava/RenderTemplate endpoint.
  • No API keys or secrets are stored; authentication uses your existing session cookie.

See REQUIREMENTS.md for the complete specification, including the response contract, error handling, and security considerations.

Deliverables

This repository contains:

  • Browser extension for Chromium (Chrome and Edge) and Firefox. This is the primary, recommended way to use the tool. It adds a toolbar button, a right-click menu item on selected text, and a keyboard shortcut, all of which work even when a Rock modal dialog is open.
  • Bookmarklet version for anyone who prefers not to install an extension (see below).
  • A shared core so both forms behave identically.
  • REQUIREMENTS.md: the full scope and requirements specification.

Repository layout

extension/        the browser extension (primary deliverable)
  core.js           canonical shared core (the translation logic)
  background.js     service worker: wires the triggers to the core
  manifest.json         Chromium (Chrome / Edge)
  manifest.firefox.json Firefox
  icons/                toolbar / store icons (16/32/48/128 px PNG)
bookmarklet/      the bookmarklet form (generated from extension/core.js)
build.mjs         generates the bookmarklet from the canonical core

The translation logic lives once, in extension/core.js. The bookmarklet is generated from it so the two forms never drift apart.

About

Plugin and Bookmarklet that translate a Rock IdKey to an Id, and back.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages