diff --git a/README.md b/README.md index cc145c3..51a96a7 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,20 @@ Likewise, the example rendered as a revealjs presentation (`presentation.qmd`) i ## Limitations -- No self-contained htmls: Molstar viewers for local files are empty when the file is **not** served by a webserver such as `quarto preview` or GitHub pages. -This means it will not display your molecule when you simply open the rendered html with a browser, -even if you set the html to be self-contained. -The reason for this is the [Same-origin Policy](https://developer.mozilla.org/en-US/docs/Glossary/Same-origin_policy), a security measure in web browsers. -It and similar policies prevent that one document can access resources it is not supposed to access. -For example, an html document you downloaded is not allowed to execute code that reads personal files on your computer. -This also prevents it from loading your molecules from local paths. -- revealjs presentations now use iframes instead of a normal div to work around https://github.com/jmbuhr/quarto-molstar/issues/1, which is why you might have to address those differently for custom styling if you plan to use the same source for html and reveal output. +- Self-contained htmls: + Molstar viewers for local files are empty when the file is **not** served by a webserver such as `quarto preview` or GitHub pages. + This means it will not display your molecule when you simply open the rendered html with a browser, + even if you set the html to be self-contained. + The reason for this is the [Same-origin Policy](https://developer.mozilla.org/en-US/docs/Glossary/Same-origin_policy), a security measure in web browsers. + It and similar policies prevent that one document can access resources it is not supposed to access. + For example, an html document you downloaded is not allowed to execute code that reads personal files on your computer. + This also prevents it from loading your molecules from local paths. + + For plain text formats in the `mol-url` shortcode, such as `pdb` and `xyz`, you can enable a custom option that circumvents this limitation + by embedding them straight into the html as a string. + Add `molstar: embed` to your yml frontmatter to use this. +- revealjs presentations now use iframes instead of a normal div to work around https://github.com/jmbuhr/quarto-molstar/issues/1, + which is why you might have to address those differently for custom styling if you plan to use the same source for html and revealjs output. ## Update Mol* (extension developement) diff --git a/_extensions/molstar/assets/app-container.css b/_extensions/molstar/assets/app-container.css index 0529fb0..d51a20d 100644 --- a/_extensions/molstar/assets/app-container.css +++ b/_extensions/molstar/assets/app-container.css @@ -5,3 +5,6 @@ div .molstar-app { margin-bottom: 1rem; } +.msp-plugin .msp-layout-expanded { + z-index: 1; +} diff --git a/_extensions/molstar/molstar.lua b/_extensions/molstar/molstar.lua index a06f3f0..68da28f 100644 --- a/_extensions/molstar/molstar.lua +++ b/_extensions/molstar/molstar.lua @@ -1,12 +1,42 @@ +-- for development: +local p = quarto.utils.dump + +---@type boolean +local useIframes = false +if quarto.doc.isFormat("revealjs") then + useIframes = true +end + +---@param path string Path to the file +---@return string|nil The file content +local function readFile(path) + local file = io.open(path, "r") + if not file then return nil end + local content = file:read "*a" + file:close() + return content +end + +---Format string like in bash or python, +---e.g. f('Hello ${one}', {one = 'world'}) +---@param s string The string to format +---@param kwargs {[string]: string} A table with key-value replacemen pairs +---@return string local function f(s, kwargs) return (s:gsub('($%b{})', function(w) return kwargs[w:sub(3, -2)] or w end)) end +---Get the file extension +---@param path string +---@return string local function fileExt(path) return path:match("[^.]+$") end -local function addDependencies(useIframes) +---Add molstar css and js dependencies. +---Can be linked / embedded for regular html documents, +---but have to be copied for revealjs to be used in iframes +local function addDependencies() quarto.doc.addHtmlDependency { name = 'molstar', version = 'v3.13.0', @@ -19,6 +49,9 @@ local function addDependencies(useIframes) end end +---Merge user provided molstar options with defaults +---@param userOptions table +---@return string JSON string to pass to molstar local function mergeMolstarOptions(userOptions) local defaultOptions = { layoutIsExpanded = false, @@ -38,7 +71,7 @@ local function mergeMolstarOptions(userOptions) end for k, v in pairs(userOptions) do - value = pandoc.utils.stringify(v) + local value = pandoc.utils.stringify(v) if value == 'true' then value = true end if value == 'false' then value = false end defaultOptions[k] = value @@ -47,151 +80,89 @@ local function mergeMolstarOptions(userOptions) return quarto.json.encode(defaultOptions) end -local function rcsbViewer(appId, pdbId, userOptions) - local subs = { appId = appId, pdb = pdbId, height = height, options = mergeMolstarOptions(userOptions) } - return f([[ - - ]], subs) +---@param viewerFunctionString string +---@return string +local function wrapInlineIframe(viewerFunctionString) + return [[ + + ]] end -local function rcsbViewerIframe(appId, pdbId, userOptions) - local frameId = 'frame' .. appId - local subs = { frameId = frameId, appId = appId, pdb = pdbId, options = mergeMolstarOptions(userOptions) } - return f([[ - - ]], subs) +---@param viewerFunctionString string +---@return string +local function wrapInlineDiv(viewerFunctionString) + return [[ +
+ + ]] end -local function urlViewer(appId, topPath, userOptions) +---@param args table +---@return string +local function createViewer(args) local subs = { - appId = appId, - top = topPath, - topExt = fileExt(topPath), - options = mergeMolstarOptions(userOptions) + appId = args.appId, + url = args.url, + pdb = args.pdbId, + trajUrl = args.trajUrl, + trajExtension = args.trajExtension, + data = args.data, + fileExtension = args.fileExtension, + options = mergeMolstarOptions(args.userOptions) } - return f([[ - - ]], subs) -end -local function urlViewerIframe(appId, topPath, userOptions) - local frameId = 'frame' .. appId - local subs = { - appId = appId, - top = topPath, - frameId = frameId, - topExt = fileExt(topPath), - options = mergeMolstarOptions(userOptions) - } - return f([[ - - ]], subs) -end + local wrapper + local viewerFunction -local function trajViewer(appId, topPath, trajPath, userOptions) - local subs = { - appId = appId, - top = topPath, - topExt = fileExt(topPath), - traj = trajPath, - trajExt = fileExt(trajPath), - options = mergeMolstarOptions(userOptions) - } - return f([[ - - ]], subs) -end + if useIframes then + wrapper = wrapInlineIframe + else + wrapper = wrapInlineDiv + end -local function trajViewerIframe(appId, topPath, trajPath, userOptions) - local frameId = 'frame' .. appId - local subs = { - appId = appId, - frameId = frameId, - top = topPath, - topExt = fileExt(topPath), - traj = trajPath, - trajExt = fileExt(trajPath), - options = mergeMolstarOptions(userOptions) - } - return f([[ - - ]], subs) + if args.data then -- if we have embedded data, use it + viewerFunction = 'viewer.loadStructureFromData(`${data}`, format="${fileExtension}");' + elseif args.pdbId then -- fetch from rcsb pdbb if an ID is given + viewerFunction = 'viewer.loadPdb("${pdb}");' + elseif args.url and args.trajUrl then -- load topology + trajectory if both are given + viewerFunction = [[ + viewer.loadTrajectory( + { + model: { + kind: "model-url", url: "${url}", format: "${fileExtension}" + }, + coordinates: { + kind: "coordinates-url", url: "${trajUrl}", + format: "${trajExtension}", isBinary: true + } + } + ); + ]] + else -- otherwise read from url (local or remote) + viewerFunction = 'viewer.loadStructureFromUrl("${url}", format="${fileExtension}");' + end + + return f(wrapper(viewerFunction), subs) end return { @@ -201,53 +172,42 @@ return { return pandoc.Null() end - if quarto.doc.isFormat("revealjs") then - useIframes = true - end - - addDependencies(useIframes) + addDependencies() local pdbId = pandoc.utils.stringify(args[1]) local appId = 'app-' .. pdbId - if useIframes then - return pandoc.RawBlock('html', rcsbViewerIframe(appId, pdbId, kwargs)) - else - return { - pandoc.Div( - {}, - { id = appId, class = 'molstar-app' } - ), - pandoc.RawBlock('html', rcsbViewer(appId, pdbId, kwargs)), - } - end + return pandoc.RawBlock('html', createViewer { + appId = appId, + pdbId = pdbId, + userOptions = kwargs + }) end, - ['mol-url'] = function(args, kwargs) + ['mol-url'] = function(args, kwargs, meta) if not quarto.doc.isFormat("html:js") then return pandoc.Null() end - if quarto.doc.isFormat("revealjs") then - useIframes = true - end - - addDependencies(useIframes) + addDependencies() - local pdbPath = pandoc.utils.stringify(args[1]) - local appId = 'app-' .. pdbPath + local url = pandoc.utils.stringify(args[1]) + local appId = 'app-' .. url + local fileExtension = fileExt(url) - if useIframes then - return pandoc.RawBlock('html', urlViewerIframe(appId, pdbPath, kwargs)) - else - return { - pandoc.Div( - {}, - { id = appId, class = 'molstar-app' } - ), - pandoc.RawBlock('html', urlViewer(appId, pdbPath, kwargs)), - } + local molstarMeta = pandoc.utils.stringify(meta['molstar']) + local pdbContent + if molstarMeta == 'embed' and not useIframes then + ---@type string|nil + pdbContent = readFile(url) end + return pandoc.RawBlock('html', createViewer { + appId = appId, + url = url, + data = pdbContent, + fileExtension = fileExtension, + userOptions = kwargs + }) end, ['mol-traj'] = function(args, kwargs) @@ -255,26 +215,19 @@ return { return pandoc.Null() end - if quarto.doc.isFormat("revealjs") then - useIframes = true - end - addDependencies() - local topPath = pandoc.utils.stringify(args[1]) - local trajPath = pandoc.utils.stringify(args[2]) - local appId = 'app-' .. topPath .. trajPath - - if useIframes then - return pandoc.RawBlock('html', trajViewerIframe(appId, topPath, trajPath, kwargs)) - else - return { - pandoc.Div( - {}, - { id = appId, class = 'molstar-app' } - ), - pandoc.RawBlock('html', trajViewer(appId, topPath, trajPath, kwargs)) - } - end + local url = pandoc.utils.stringify(args[1]) + local trajUrl = pandoc.utils.stringify(args[2]) + local appId = 'app-' .. url .. trajUrl + + return pandoc.RawBlock('html', createViewer { + appId = appId, + url = url, + trajUrl = trajUrl, + fileExtension = fileExt(url), + trajExtension = fileExt(trajUrl), + userOptions = kwargs + }) end, } diff --git a/index.html b/index.html index 84e5694..dbd8396 100644 --- a/index.html +++ b/index.html @@ -11,10 +11,15 @@