al_marimo 1.0.0
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 +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +21 -0
- data/README.md +90 -0
- data/lib/al_marimo/version.rb +5 -0
- data/lib/al_marimo.rb +196 -0
- data/lib/assets/al_marimo/css/marimo.css +42 -0
- data/lib/assets/al_marimo/js/marimo-init.js +114 -0
- data/lib/assets/al_marimo/js/marimo-snippets.js +213 -0
- data/lib/vendor/provenance.json +10 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d04f121dc6cb7e4d4d132f0ba0e7912df875b73707013bfa35a66a5f5be8586b
|
|
4
|
+
data.tar.gz: a44b52b29146749d4e7a754de13530670a0f0c48d718bfa6a40fd954e9e52a4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 93d3a014fa33632f6fdb78ed349388c94ebf282bf59f0c54e354759783589d97cc7ce2c8ff5c7880be8ec2be6080430c8a0dacacca891ff7a435b008b9e82fba
|
|
7
|
+
data.tar.gz: f177be12c37a379ee733af04c910f80e3bbd961cb8faa8e60ffa5aee52a2cc04a76d89decd3f353214a76184665ec4369c435097298938645a96871b71110c11
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
- Initial release of `al_marimo`, implementing the plugin proposed in
|
|
6
|
+
[alshedivat/al-folio#3541](https://github.com/alshedivat/al-folio/issues/3541) (source PR
|
|
7
|
+
[#3517](https://github.com/alshedivat/al-folio/pull/3517)).
|
|
8
|
+
- `{% al_marimo_embed %}` for hosted notebooks and `.al-marimo-inline` conversion for in-page Python snippets.
|
|
9
|
+
- The `marimo-snippets` runtime is **vendored and version-pinned** with recorded provenance, rather than loaded from a
|
|
10
|
+
CDN at an unpinned major version as the source PR did.
|
|
11
|
+
- Embedded notebooks are sandboxed **without** `allow-same-origin`.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) al-folio maintainers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# al_marimo
|
|
2
|
+
|
|
3
|
+
[marimo](https://marimo.io) interactive Python notebooks for [al-folio](https://github.com/alshedivat/al-folio) v1.x.
|
|
4
|
+
Implements the plugin proposed in [al-folio#3541](https://github.com/alshedivat/al-folio/issues/3541).
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
gem "al_marimo", "= 1.0.0"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
and in `_config.yml` — **both lists, or the plugin is inert**:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
plugins:
|
|
16
|
+
- al_marimo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then opt in **per page**, in front matter:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
---
|
|
23
|
+
layout: post
|
|
24
|
+
title: a notebook post
|
|
25
|
+
marimo: true
|
|
26
|
+
---
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Nothing is loaded on pages without that flag, so bundling the plugin costs nothing site-wide.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Embedding a hosted notebook
|
|
34
|
+
|
|
35
|
+
```liquid
|
|
36
|
+
{% al_marimo_embed src="https://marimo.app/l/abc123" height="700px" caption="Sampling demo" %}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Accepts `src` (required), `height`, `width`, `mode` (`read` / `edit`), `embed`, `title`, `caption`, `class`. `src` may
|
|
40
|
+
be a literal or a Liquid variable.
|
|
41
|
+
|
|
42
|
+
### Inline runnable snippets
|
|
43
|
+
|
|
44
|
+
Wrap fenced Python blocks in a container:
|
|
45
|
+
|
|
46
|
+
<pre>
|
|
47
|
+
<div class="al-marimo-inline" markdown="1">
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import marimo as mo
|
|
51
|
+
mo.md("Hello from the browser")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
</pre>
|
|
56
|
+
|
|
57
|
+
The runtime moves those blocks into a `<marimo-iframe>` after load.
|
|
58
|
+
|
|
59
|
+
## Differences from the source PR
|
|
60
|
+
|
|
61
|
+
The source PR was explicitly a work in progress. Two things changed:
|
|
62
|
+
|
|
63
|
+
**The runtime is vendored, not fetched from a CDN.** The PR loaded
|
|
64
|
+
`https://cdn.jsdelivr.net/npm/@marimo-team/marimo-snippets@1` — an unpinned major version, with no integrity hash,
|
|
65
|
+
executing in every visitor's page. That is the shape of the polyfill.io supply-chain attack. Pinning it with SRI is not
|
|
66
|
+
possible either: that URL serves a _dynamically minified_ build, and jsDelivr
|
|
67
|
+
[documents that such files must not be used with SRI](https://www.jsdelivr.com/using-sri-with-dynamic-files) because a
|
|
68
|
+
minifier upgrade changes the bytes and would break every site at once. So the stable unminified source is vendored at an
|
|
69
|
+
exact version, with its digest recorded in [`lib/vendor/provenance.json`](lib/vendor/provenance.json) and a test that
|
|
70
|
+
fails if the shipped file drifts from it.
|
|
71
|
+
|
|
72
|
+
**Embedded notebooks do not get `allow-same-origin`.** The PR's sandbox included it, which lets the embedded
|
|
73
|
+
third-party notebook reach the embedding page's storage and cookies — defeating the purpose of the sandbox.
|
|
74
|
+
|
|
75
|
+
## Note on third-party contact
|
|
76
|
+
|
|
77
|
+
The notebook itself executes on `marimo.app` (or your own WASM host). Embedding one means your readers' browsers contact
|
|
78
|
+
that origin. The vendored runtime also references a marimo-hosted icon. Nothing is sent unless a page opts in.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle install
|
|
84
|
+
bundle exec rake test
|
|
85
|
+
npm ci && npm run lint:prettier
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
data/lib/al_marimo.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "json"
|
|
5
|
+
require "jekyll"
|
|
6
|
+
require "liquid"
|
|
7
|
+
require_relative "al_marimo/version"
|
|
8
|
+
|
|
9
|
+
# Embeds marimo notebooks, and turns Python code blocks into runnable snippets.
|
|
10
|
+
#
|
|
11
|
+
# Two-layer gating, per the al-folio contract: the gem must be loaded *and* the
|
|
12
|
+
# page must opt in with `marimo: true` in its front matter. Without the page
|
|
13
|
+
# opt-in every tag renders an empty string, so a site that bundles this plugin
|
|
14
|
+
# pays nothing on pages that do not use it.
|
|
15
|
+
module AlMarimo
|
|
16
|
+
PLUGIN_ROOT = File.expand_path("..", __dir__)
|
|
17
|
+
# Jekyll writes a StaticFile to <dest>/<dir>/<name>, where <dir> is relative
|
|
18
|
+
# to the base below. That base must be lib/, not the gem root, or assets land
|
|
19
|
+
# at /lib/assets/... while every tag in this file points at /assets/... .
|
|
20
|
+
LIB_ROOT = __dir__
|
|
21
|
+
ASSETS_ROOT = File.join(LIB_ROOT, "assets")
|
|
22
|
+
VENDOR_ROOT = File.join(PLUGIN_ROOT, "lib", "vendor")
|
|
23
|
+
|
|
24
|
+
# marimo runs the notebook itself; only these origins are ever contacted.
|
|
25
|
+
DEFAULT_PLAYGROUND = "https://marimo.app"
|
|
26
|
+
|
|
27
|
+
class PluginStaticFile < Jekyll::StaticFile; end
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def config(site)
|
|
32
|
+
return {} unless site
|
|
33
|
+
|
|
34
|
+
site.config["al_marimo"] || {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Page-level opt-in. Anything truthy other than the string "false" counts, so
|
|
38
|
+
# `marimo: true` and `marimo: snippets` both work.
|
|
39
|
+
#
|
|
40
|
+
# Reads through `[]` rather than testing for Hash: at render time Jekyll hands
|
|
41
|
+
# `registers[:page]` a Drop (Jekyll::Drops::DocumentDrop), not a Hash, so an
|
|
42
|
+
# `is_a?(Hash)` guard silently discards the front matter and the feature never
|
|
43
|
+
# switches on for any real page.
|
|
44
|
+
def page_enabled?(page)
|
|
45
|
+
value = begin
|
|
46
|
+
page.respond_to?(:[]) ? page["marimo"] : nil
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
return false if value.nil? || value == false
|
|
51
|
+
return false if value.respond_to?(:strip) && ["", "false"].include?(value.strip.downcase)
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def provenance
|
|
57
|
+
@provenance ||= JSON.parse(File.read(File.join(VENDOR_ROOT, "provenance.json")))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def escape(value)
|
|
61
|
+
CGI.escapeHTML(value.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Builds the notebook URL, folding in the query parameters marimo understands.
|
|
65
|
+
#
|
|
66
|
+
# Appends with the correct separator rather than assuming the caller passed a
|
|
67
|
+
# bare URL: `?embed=true` on a URL that already has a query string produces a
|
|
68
|
+
# link that silently loads the wrong notebook.
|
|
69
|
+
def build_url(src, embed: true, mode: "read")
|
|
70
|
+
url = src.to_s.strip
|
|
71
|
+
return nil if url.empty?
|
|
72
|
+
|
|
73
|
+
separator = url.include?("?") ? "&" : "?"
|
|
74
|
+
if embed
|
|
75
|
+
url = "#{url}#{separator}embed=true"
|
|
76
|
+
separator = "&"
|
|
77
|
+
end
|
|
78
|
+
"#{url}#{separator}mode=#{mode == "edit" ? "edit" : "read"}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# [relative_dir, filename] for everything this gem publishes. Exposed so the
|
|
82
|
+
# destination path can be asserted without booting a full Jekyll site.
|
|
83
|
+
def asset_entries
|
|
84
|
+
Dir.glob(File.join(ASSETS_ROOT, "**", "*")).sort.reject { |p| File.directory?(p) }.map do |source_path|
|
|
85
|
+
[File.dirname(source_path).sub("#{LIB_ROOT}/", ""), File.basename(source_path)]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class AssetsGenerator < Jekyll::Generator
|
|
90
|
+
safe true
|
|
91
|
+
priority :low
|
|
92
|
+
|
|
93
|
+
def generate(site)
|
|
94
|
+
Dir.glob(File.join(ASSETS_ROOT, "**", "*")).sort.each do |source_path|
|
|
95
|
+
next if File.directory?(source_path)
|
|
96
|
+
|
|
97
|
+
relative_dir = File.dirname(source_path).sub("#{LIB_ROOT}/", "")
|
|
98
|
+
site.static_files << PluginStaticFile.new(site, LIB_ROOT, relative_dir, File.basename(source_path))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# {% al_marimo_scripts %} — emits the runtime, only on pages that opted in.
|
|
104
|
+
class ScriptsTag < Liquid::Tag
|
|
105
|
+
def render(context)
|
|
106
|
+
site = context.registers[:site]
|
|
107
|
+
page = context.registers[:page]
|
|
108
|
+
return "" unless site && AlMarimo.page_enabled?(page)
|
|
109
|
+
|
|
110
|
+
baseurl = site.config["baseurl"] || ""
|
|
111
|
+
# Vendored rather than pulled from a CDN. jsDelivr serves a dynamically
|
|
112
|
+
# minified build at the package root and documents that it must not be
|
|
113
|
+
# used with SRI; loading it unpinned would hand every al-folio site's
|
|
114
|
+
# execution context to whatever that URL returns tomorrow.
|
|
115
|
+
<<~HTML
|
|
116
|
+
<script defer src="#{baseurl}/assets/al_marimo/js/marimo-init.js"></script>
|
|
117
|
+
<script defer src="#{baseurl}/assets/al_marimo/js/marimo-snippets.js"></script>
|
|
118
|
+
HTML
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class StylesTag < Liquid::Tag
|
|
123
|
+
def render(context)
|
|
124
|
+
site = context.registers[:site]
|
|
125
|
+
page = context.registers[:page]
|
|
126
|
+
return "" unless site && AlMarimo.page_enabled?(page)
|
|
127
|
+
|
|
128
|
+
baseurl = site.config["baseurl"] || ""
|
|
129
|
+
%(<link rel="stylesheet" href="#{baseurl}/assets/al_marimo/css/marimo.css">\n)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# {% al_marimo_embed src="https://marimo.app/l/abc" height="600px" caption="..." %}
|
|
134
|
+
class EmbedTag < Liquid::Tag
|
|
135
|
+
ATTRIBUTES = /(\w+)\s*=\s*("[^"]*"|'[^']*'|[^\s]+)/.freeze
|
|
136
|
+
|
|
137
|
+
def initialize(tag_name, markup, options)
|
|
138
|
+
super
|
|
139
|
+
@attributes = {}
|
|
140
|
+
markup.scan(ATTRIBUTES) do |key, value|
|
|
141
|
+
@attributes[key] = value.gsub(/\A["']|["']\z/, "")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def render(context)
|
|
146
|
+
site = context.registers[:site]
|
|
147
|
+
return "" unless site
|
|
148
|
+
|
|
149
|
+
src = resolve(@attributes["src"], context)
|
|
150
|
+
url = AlMarimo.build_url(
|
|
151
|
+
src,
|
|
152
|
+
embed: @attributes.fetch("embed", "true") != "false",
|
|
153
|
+
mode: @attributes["mode"] || "read"
|
|
154
|
+
)
|
|
155
|
+
# A missing or empty src is an authoring mistake. Emit nothing rather than
|
|
156
|
+
# an iframe pointed at the site itself, which would silently render the
|
|
157
|
+
# page inside its own post.
|
|
158
|
+
return "" if url.nil?
|
|
159
|
+
|
|
160
|
+
height = @attributes["height"] || "600px"
|
|
161
|
+
width = @attributes["width"] || "100%"
|
|
162
|
+
title = @attributes["title"] || "marimo notebook"
|
|
163
|
+
caption = @attributes["caption"]
|
|
164
|
+
extra_class = @attributes["class"]
|
|
165
|
+
|
|
166
|
+
figure = +%(<figure class="al-marimo">)
|
|
167
|
+
figure << %(<div class="al-marimo-embed">)
|
|
168
|
+
figure << %(<iframe src="#{AlMarimo.escape(url)}")
|
|
169
|
+
figure << %( class="#{AlMarimo.escape(extra_class)}") if extra_class
|
|
170
|
+
# Deliberately omits allow-same-origin: the notebook is third-party code,
|
|
171
|
+
# and granting it same-origin access to the embedding site would let it
|
|
172
|
+
# read the parent document's storage and cookies.
|
|
173
|
+
figure << %( sandbox="allow-scripts allow-downloads allow-popups allow-forms")
|
|
174
|
+
figure << %( width="#{AlMarimo.escape(width)}" height="#{AlMarimo.escape(height)}")
|
|
175
|
+
figure << %( title="#{AlMarimo.escape(title)}" loading="lazy" frameborder="0" allowfullscreen></iframe>)
|
|
176
|
+
figure << %(</div>)
|
|
177
|
+
figure << %(<figcaption class="caption">#{AlMarimo.escape(caption)}</figcaption>) if caption
|
|
178
|
+
figure << %(</figure>)
|
|
179
|
+
figure
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
# Allows both a literal (src="https://…") and a variable (src=page.notebook).
|
|
185
|
+
def resolve(value, context)
|
|
186
|
+
return nil if value.nil?
|
|
187
|
+
|
|
188
|
+
looked_up = context[value]
|
|
189
|
+
looked_up.nil? ? value : looked_up
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
Liquid::Template.register_tag("al_marimo_scripts", AlMarimo::ScriptsTag)
|
|
195
|
+
Liquid::Template.register_tag("al_marimo_styles", AlMarimo::StylesTag)
|
|
196
|
+
Liquid::Template.register_tag("al_marimo_embed", AlMarimo::EmbedTag)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Styling for marimo embeds. Plain CSS rather than Sass: _sass/ is gem-owned by
|
|
2
|
+
al_folio_core and a plugin must not add to the theme's build pipeline. */
|
|
3
|
+
|
|
4
|
+
.al-marimo {
|
|
5
|
+
margin: 1.5rem 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.al-marimo-embed {
|
|
9
|
+
position: relative;
|
|
10
|
+
width: 100%;
|
|
11
|
+
/* Notebooks are wide; let the frame scroll itself rather than the page. */
|
|
12
|
+
overflow-x: auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.al-marimo-embed iframe {
|
|
16
|
+
display: block;
|
|
17
|
+
width: 100%;
|
|
18
|
+
border: 1px solid var(--global-divider-color, rgba(0, 0, 0, 0.1));
|
|
19
|
+
border-radius: 6px;
|
|
20
|
+
background: var(--global-bg-color, transparent);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.al-marimo figcaption,
|
|
24
|
+
.al-marimo .caption {
|
|
25
|
+
margin-top: 0.5rem;
|
|
26
|
+
font-size: 0.875rem;
|
|
27
|
+
text-align: center;
|
|
28
|
+
color: var(--global-text-color-light, inherit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Hidden until marimo-init.js has moved the code blocks into <marimo-iframe>,
|
|
32
|
+
so readers never see the raw source flash before it becomes a notebook.
|
|
33
|
+
Scripts are deferred, so this is re-shown on DOMContentLoaded at the latest. */
|
|
34
|
+
.al-marimo-inline {
|
|
35
|
+
visibility: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Without JS the block would stay hidden forever, which is worse than showing
|
|
39
|
+
the code. Reveal it and let it render as an ordinary listing. */
|
|
40
|
+
.no-js .al-marimo-inline {
|
|
41
|
+
visibility: visible;
|
|
42
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Prepares kramdown's code-block output for marimo-snippets.
|
|
2
|
+
//
|
|
3
|
+
// marimo-snippets reads <pre> children of a <marimo-iframe>. Kramdown wraps
|
|
4
|
+
// fenced blocks in a .highlighter-rouge div whose nested elements contribute
|
|
5
|
+
// whitespace-only text nodes; marimo-snippets reads the element via textContent
|
|
6
|
+
// and those nodes show up as blank leading and trailing lines in the notebook.
|
|
7
|
+
// So the <pre> is moved out of the wrapper rather than the wrapper moved in.
|
|
8
|
+
//
|
|
9
|
+
// Runs before marimo-snippets (which is loaded after it) and is idempotent, so
|
|
10
|
+
// a page that re-runs it does not double-convert.
|
|
11
|
+
(function () {
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
var LANGUAGE_PREFIX = "language-";
|
|
15
|
+
|
|
16
|
+
function languageOf(element) {
|
|
17
|
+
var found = "";
|
|
18
|
+
Array.prototype.forEach.call(element.classList, function (name) {
|
|
19
|
+
if (name.indexOf(LANGUAGE_PREFIX) === 0) {
|
|
20
|
+
found = name;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Kramdown emits a trailing newline inside <code>; marimo renders it as an
|
|
27
|
+
// empty final cell line.
|
|
28
|
+
function trim(pre) {
|
|
29
|
+
Array.prototype.forEach.call(pre.querySelectorAll("code"), function (code) {
|
|
30
|
+
code.textContent = code.textContent.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectBlocks(container) {
|
|
35
|
+
var wrappers = container.querySelectorAll(".highlighter-rouge");
|
|
36
|
+
var blocks = [];
|
|
37
|
+
|
|
38
|
+
if (wrappers.length > 0) {
|
|
39
|
+
Array.prototype.forEach.call(wrappers, function (wrapper) {
|
|
40
|
+
var language = languageOf(wrapper);
|
|
41
|
+
var pre = wrapper.querySelector("pre");
|
|
42
|
+
if (!pre) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// marimo-snippets distinguishes Python cells from markdown cells by the
|
|
46
|
+
// language class, which kramdown puts on the wrapper, not the <pre>.
|
|
47
|
+
if (language) {
|
|
48
|
+
pre.classList.add(language);
|
|
49
|
+
}
|
|
50
|
+
trim(pre);
|
|
51
|
+
blocks.push(pre);
|
|
52
|
+
});
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Array.prototype.forEach.call(container.querySelectorAll("pre"), function (pre) {
|
|
57
|
+
trim(pre);
|
|
58
|
+
blocks.push(pre);
|
|
59
|
+
});
|
|
60
|
+
return blocks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Height is a guess, but a bad guess is very visible: too short and the
|
|
64
|
+
// notebook scrolls inside a stub. Scale with both cell count and line count.
|
|
65
|
+
function estimateHeight(blocks) {
|
|
66
|
+
var lines = blocks.reduce(function (total, pre) {
|
|
67
|
+
return total + (pre.textContent || "").split("\n").filter(Boolean).length;
|
|
68
|
+
}, 0);
|
|
69
|
+
return Math.max(300, 150 + blocks.length * 80 + lines * 22);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function convert(container) {
|
|
73
|
+
if (container.dataset.alMarimoReady === "true") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var blocks = collectBlocks(container);
|
|
78
|
+
if (blocks.length === 0) {
|
|
79
|
+
// Nothing to run. Leave the markup alone and make it visible again rather
|
|
80
|
+
// than stranding the reader with a permanently hidden block.
|
|
81
|
+
container.dataset.alMarimoReady = "true";
|
|
82
|
+
container.style.visibility = "visible";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var frame = document.createElement("marimo-iframe");
|
|
87
|
+
Array.prototype.forEach.call(container.attributes, function (attribute) {
|
|
88
|
+
if (attribute.name.indexOf("data-") === 0 && attribute.name !== "data-al-marimo-ready") {
|
|
89
|
+
frame.setAttribute(attribute.name, attribute.value);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
blocks.forEach(function (pre) {
|
|
93
|
+
frame.appendChild(pre);
|
|
94
|
+
});
|
|
95
|
+
if (!frame.hasAttribute("data-height")) {
|
|
96
|
+
frame.setAttribute("data-height", estimateHeight(blocks) + "px");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
container.textContent = "";
|
|
100
|
+
container.appendChild(frame);
|
|
101
|
+
container.dataset.alMarimoReady = "true";
|
|
102
|
+
container.style.visibility = "visible";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ready() {
|
|
106
|
+
Array.prototype.forEach.call(document.querySelectorAll(".al-marimo-inline"), convert);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (document.readyState === "loading") {
|
|
110
|
+
document.addEventListener("DOMContentLoaded", ready);
|
|
111
|
+
} else {
|
|
112
|
+
ready();
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
let buttonSettings = {
|
|
2
|
+
elements: ['pre'],
|
|
3
|
+
title: 'Open code in an interactive playground',
|
|
4
|
+
position: 'absolute',
|
|
5
|
+
top: '0.5rem',
|
|
6
|
+
right: '0.5rem',
|
|
7
|
+
border: 'none',
|
|
8
|
+
borderRadius: '4px',
|
|
9
|
+
padding: '4px 8px',
|
|
10
|
+
margin: '-4px 22px',
|
|
11
|
+
cursor: 'pointer',
|
|
12
|
+
zIndex: '10',
|
|
13
|
+
filter: 'grayscale(100%)',
|
|
14
|
+
icon: '<img src="https://cms.marimo.io/icons/favicon.svg" alt="icon" width="20" height="20">',
|
|
15
|
+
url: 'https://marimo.app',
|
|
16
|
+
paramName: 'code'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let iframeSettings = {
|
|
20
|
+
elements: ['pre'],
|
|
21
|
+
height: '400px',
|
|
22
|
+
width: '100%',
|
|
23
|
+
border: '1px solid #ddd',
|
|
24
|
+
borderRadius: '4px',
|
|
25
|
+
margin: '1rem 0',
|
|
26
|
+
showCode: 'true',
|
|
27
|
+
url: 'https://marimo.app',
|
|
28
|
+
paramName: 'code'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Configure interactive buttons for code blocks that open the code in a Marimo playground
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} settings - Button customization options
|
|
35
|
+
* @param {string[]} [settings.elements=['pre', 'div.highlight'] - CSS selectors for elements to add buttons to. Default: ['pre', 'div.highlight']
|
|
36
|
+
* @param {string} [settings.title='Open code in an interactive playground'] - Button tooltip text
|
|
37
|
+
* @param {string} [settings.position='absolute'] - CSS position property
|
|
38
|
+
* @param {string} [settings.top='0.5rem'] - Distance from top of container
|
|
39
|
+
* @param {string} [settings.right='0.5rem'] - Distance from right of container
|
|
40
|
+
* @param {string} [settings.border='none'] - Button border style
|
|
41
|
+
* @param {string} [settings.borderRadius='4px'] - Button corner radius
|
|
42
|
+
* @param {string} [settings.padding='4px 8px'] - Button padding
|
|
43
|
+
* @param {string} [settings.margin='-4px 22px'] - Button margin
|
|
44
|
+
* @param {string} [settings.cursor='pointer'] - Mouse cursor style on hover
|
|
45
|
+
* @param {string} [settings.zIndex='10'] - Button stacking order
|
|
46
|
+
* @param {string} [settings.filter='grayscale(100%)'] - Default filter applied to button
|
|
47
|
+
* @param {string} [settings.icon='<img src="https://cms.marimo.io/icons/favicon.svg" alt="icon" width="20" height="20">'] - HTML content for the button
|
|
48
|
+
* @param {string} [settings.url='https://marimo.app'] - Base URL for the Marimo instance
|
|
49
|
+
* @param {string} [settings.paramName='code'] - Query parameter name for the code
|
|
50
|
+
*/
|
|
51
|
+
function configureMarimoButtons(settings = {}) {
|
|
52
|
+
buttonSettings = { ...buttonSettings, ...settings };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configure marimo iframes
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} settings - Iframe customization options
|
|
59
|
+
* @param {string[]} [settings.elements=['pre', 'div.highlight'] - CSS selectors for elements to add buttons to. Default: ['pre', 'div.highlight']
|
|
60
|
+
* @param {string} [settings.height='400px'] - Height of the iframe
|
|
61
|
+
* @param {string} [settings.width='100%'] - Width of the iframe
|
|
62
|
+
* @param {string} [settings.border='1px solid #ddd'] - Border style of the iframe
|
|
63
|
+
* @param {string} [settings.borderRadius='4px'] - Corner radius of the iframe
|
|
64
|
+
* @param {string} [settings.margin='1rem 0'] - Margin around the iframe
|
|
65
|
+
* @param {string} [settings.showCode='true'] - Whether to show the notebook's code
|
|
66
|
+
* @param {string} [settings.url='https://marimo.app'] - Base URL for the Marimo instance
|
|
67
|
+
* @param {string} [settings.paramName='code'] - Query parameter name for the code
|
|
68
|
+
*/
|
|
69
|
+
function configureMarimoIframes(settings = {}) {
|
|
70
|
+
iframeSettings = { ...iframeSettings, ...settings };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateCell(code, kind="python") {
|
|
74
|
+
|
|
75
|
+
if (kind === "python") {
|
|
76
|
+
return `@app.cell
|
|
77
|
+
def _():
|
|
78
|
+
${code.split('\n').map(line => ' ' + line).join('\n')}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
if (kind === "md") {
|
|
82
|
+
return `@app.cell(hide_code=True)
|
|
83
|
+
def _():
|
|
84
|
+
mo.md("""
|
|
85
|
+
${code.split('\n').map(line => ' ' + line).join('\n')}
|
|
86
|
+
""")
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateNotebook(cells) {
|
|
92
|
+
return `import marimo
|
|
93
|
+
|
|
94
|
+
app = marimo.App()
|
|
95
|
+
|
|
96
|
+
${cells}
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Helper to override settings using data attributes.
|
|
102
|
+
* It takes an element and a settings object and returns a new configuration,
|
|
103
|
+
* where any data-* attribute (e.g. data-title or data-elements) on the element
|
|
104
|
+
* will override the corresponding property in the settings.
|
|
105
|
+
*/
|
|
106
|
+
function overrideSettingsWithDataAttributes(element, settings) {
|
|
107
|
+
const config = { ...settings };
|
|
108
|
+
for (const key in element.dataset) {
|
|
109
|
+
let value = element.dataset[key];
|
|
110
|
+
// If key is "elements", assume a comma-separated list.
|
|
111
|
+
if (key.toLowerCase() === "elements") {
|
|
112
|
+
value = value.split(',').map(s => s.trim());
|
|
113
|
+
}
|
|
114
|
+
config[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createButton(codeElement, config = buttonSettings) {
|
|
120
|
+
const button = document.createElement('button');
|
|
121
|
+
button.className = 'url-copy-button';
|
|
122
|
+
button.title = config.title;
|
|
123
|
+
button.style.position = config.position;
|
|
124
|
+
button.style.top = config.top;
|
|
125
|
+
button.style.right = config.right;
|
|
126
|
+
button.style.border = config.border;
|
|
127
|
+
button.style.borderRadius = config.borderRadius;
|
|
128
|
+
button.style.padding = config.padding;
|
|
129
|
+
button.style.margin = config.margin;
|
|
130
|
+
button.style.cursor = config.cursor;
|
|
131
|
+
button.style.zIndex = config.zIndex;
|
|
132
|
+
button.style.filter = config.filter;
|
|
133
|
+
button.innerHTML = config.icon;
|
|
134
|
+
|
|
135
|
+
button.addEventListener("mouseover", function() {
|
|
136
|
+
button.style.filter = "grayscale(0%)";
|
|
137
|
+
});
|
|
138
|
+
button.addEventListener("mouseout", function() {
|
|
139
|
+
button.style.filter = config.filter;
|
|
140
|
+
});
|
|
141
|
+
button.addEventListener('click', function(e) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
const code = generateNotebook(generateCell(codeElement.textContent));
|
|
144
|
+
const encodedCode = encodeURIComponent(code);
|
|
145
|
+
const url = `${config.url}?${config.paramName}=${encodedCode}`;
|
|
146
|
+
window.open(url, '_blank');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return button;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Adds interactive buttons to code blocks that open the code in a Marimo playground.
|
|
154
|
+
* This uses any data attributes on <marimo-button> to override defaults.
|
|
155
|
+
*/
|
|
156
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
157
|
+
document.querySelectorAll('script[type="text/x-marimo-snippets-config"]').forEach(script => {
|
|
158
|
+
eval(script.textContent);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const buttons = document.querySelectorAll('marimo-button');
|
|
162
|
+
buttons.forEach(button => {
|
|
163
|
+
// Merge data attribute config with global buttonSettings.
|
|
164
|
+
const buttonConfig = overrideSettingsWithDataAttributes(button, buttonSettings);
|
|
165
|
+
const preElement = button.querySelector(buttonConfig.elements.join(","));
|
|
166
|
+
if (!preElement) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Ensure the pre element has a relative position for button placement.
|
|
170
|
+
if (getComputedStyle(preElement).position === 'static') {
|
|
171
|
+
preElement.style.position = 'relative';
|
|
172
|
+
}
|
|
173
|
+
preElement.appendChild(createButton(preElement, buttonConfig));
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Replaces code blocks with inline marimo notebooks.
|
|
179
|
+
* This uses any data attributes on <marimo-iframe> to override defaults.
|
|
180
|
+
*/
|
|
181
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
182
|
+
const marimoFrames = document.querySelectorAll("marimo-iframe");
|
|
183
|
+
marimoFrames.forEach(marimoFrame => {
|
|
184
|
+
// Merge data attribute config with global iframeSettings.
|
|
185
|
+
const iframeConfig = overrideSettingsWithDataAttributes(marimoFrame, iframeSettings);
|
|
186
|
+
|
|
187
|
+
console.log("marimoFrame", marimoFrame);
|
|
188
|
+
|
|
189
|
+
const cells = Array.from(marimoFrame.children).map((element) => {
|
|
190
|
+
const allClassNames = Array.from(element.classList).concat(
|
|
191
|
+
Array.from(element.getElementsByTagName("*")).flatMap(el => Array.from(el.classList))
|
|
192
|
+
);
|
|
193
|
+
const kind = allClassNames.includes("language-python") ? "python" : "md";
|
|
194
|
+
|
|
195
|
+
return generateCell(element.textContent, kind);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const code = generateNotebook(cells.join("\n"));
|
|
199
|
+
|
|
200
|
+
const iframe = document.createElement('iframe');
|
|
201
|
+
iframe.style.height = iframeConfig.height;
|
|
202
|
+
iframe.style.width = iframeConfig.width;
|
|
203
|
+
iframe.style.border = iframeConfig.border;
|
|
204
|
+
iframe.style.borderRadius = iframeConfig.borderRadius;
|
|
205
|
+
iframe.style.margin = iframeConfig.margin;
|
|
206
|
+
|
|
207
|
+
const encodedCode = encodeURIComponent(code);
|
|
208
|
+
const mode = iframeConfig.showCode === 'false' ? 'read' : 'edit';
|
|
209
|
+
const url = `${iframeConfig.url}?${iframeConfig.paramName}=${encodedCode}&embed=true&show-chrome=false&mode=${mode}&show-code=${iframeConfig.showCode}`;
|
|
210
|
+
iframe.src = url;
|
|
211
|
+
marimoFrame.replaceWith(iframe);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marimo-team/marimo-snippets",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"source_url": "https://cdn.jsdelivr.net/npm/@marimo-team/marimo-snippets@1.0.3/src/extractor.js",
|
|
5
|
+
"sha384": "sha384-wu02bTA++0bnvSB8GwNxnJmRB7AYFTgpaidK3uEVGrV6yu6QcdOt+J9vtHvla1vZ",
|
|
6
|
+
"sha256": "QENbXBT2k95BXBwOn5Ctx6Pr0cp46cyYWx2gRMv7vqw=",
|
|
7
|
+
"bytes": 8042,
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"note": "Vendored rather than loaded from a CDN. jsDelivr's default package URL serves a dynamically minified build and documents that it must not be used with SRI, since a Terser bump changes the hash and breaks every site at once. The unminified source path is stable, so it is pinned and vendored here."
|
|
10
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: al_marimo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- al-folio maintainers
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jekyll
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.9'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '3.9'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: liquid
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.0'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '6.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '4.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '6.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: bundler
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.0'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '3.0'
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '2.0'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.0'
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: rake
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '13.0'
|
|
80
|
+
type: :development
|
|
81
|
+
prerelease: false
|
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '13.0'
|
|
87
|
+
- !ruby/object:Gem::Dependency
|
|
88
|
+
name: minitest
|
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '5.0'
|
|
94
|
+
type: :development
|
|
95
|
+
prerelease: false
|
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '5.0'
|
|
101
|
+
description: Embeds marimo notebooks and turns Python code blocks into runnable in-browser
|
|
102
|
+
snippets.
|
|
103
|
+
email:
|
|
104
|
+
- maintainers@al-folio.dev
|
|
105
|
+
executables: []
|
|
106
|
+
extensions: []
|
|
107
|
+
extra_rdoc_files: []
|
|
108
|
+
files:
|
|
109
|
+
- CHANGELOG.md
|
|
110
|
+
- LICENSE
|
|
111
|
+
- README.md
|
|
112
|
+
- lib/al_marimo.rb
|
|
113
|
+
- lib/al_marimo/version.rb
|
|
114
|
+
- lib/assets/al_marimo/css/marimo.css
|
|
115
|
+
- lib/assets/al_marimo/js/marimo-init.js
|
|
116
|
+
- lib/assets/al_marimo/js/marimo-snippets.js
|
|
117
|
+
- lib/vendor/provenance.json
|
|
118
|
+
homepage: https://github.com/al-org-dev/al-marimo
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata:
|
|
122
|
+
allowed_push_host: https://rubygems.org
|
|
123
|
+
homepage_uri: https://github.com/al-org-dev/al-marimo
|
|
124
|
+
source_code_uri: https://github.com/al-org-dev/al-marimo
|
|
125
|
+
post_install_message:
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '2.7'
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 3.5.22
|
|
141
|
+
signing_key:
|
|
142
|
+
specification_version: 4
|
|
143
|
+
summary: Marimo interactive notebook plugin for al-folio v1.x
|
|
144
|
+
test_files: []
|