a = Div(P('a'), P('aa'), id='a11')
b = P('b', Span('bb'))
test_eq(_to_js(a), ['div', [['p', ['a'], {}], ['p', ['aa'], {}]], {'id': 'a11'}])
test_eq(_to_js(b), ['p', ['b', ['span', ['bb'], {}]], {}])
test_eq(to_js(a, 'const a=`${"a"}`; console.log(a)', b), """[
['div', [['p', ['a'], {}], ['p', ['aa'], {}]], {'id': 'a11'}],
'const a=`${"a"}`; console.log(a)',
['p', ['b', ['span', ['bb'], {}]], {}]
]""")Bridge helpers
Helpers
Internal utilities for JS/Python interop. Most users won’t need these directly.
notdebug
notdebug (jsstr:str)
debug
debug (jsstr:str)
to_js
to_js (*fts:fastcore.xml.FT|str)
a = Script('const a=`${"a"}`; console.log(a)')
b = Script(src='https://unpkg.com/htmx.org@next/dist/htmx.js', type='module')
test_eq(repr(_to_js(a)), "['script', ['const a=`${\"a\"}`; console.log(a)'], {}]")
test_eq(to_js(a, b), """[
['script', ['const a=`${"a"}`; console.log(a)'], {}],
['script', [''], {'src': 'https://unpkg.com/htmx.org@next/dist/htmx.js', 'type': 'module'}]
]""")Quick & dirty way to convert FT to HTML elements int the front-end using fasthtml-js $E.
Intended only for “linking”/head elements (with void or text content): script, style, link, meta, etc. It’ll surely fail with other elements.
ScriptsDetails
ScriptsDetails (scs, title='Loaded scripts', open=True)
Initialize self. See help(type(self)) for accurate signature.
Bridge scripts
HTMX and other useful JS libraries.
show_scripts
show_scripts (**scs:fastcore.xml.FT)
bridge_scripts
bridge_scripts (htmx=True)
bridge_scripts(){'htmx': script(('',),{'src': 'https://unpkg.com/htmx.org@next/dist/htmx.js'}),
'fasthtml_js': script((),{'src': 'https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js'}),
'surreal': script((),{'src': 'https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js'}),
'css_scope_inline': script((),{'src': 'https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js'})}
show_scripts(**bridge_scripts())Loaded scripts
<script src="https://unpkg.com/htmx.org@next/dist/htmx.js"></script> <script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script> <script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script> <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
BCanvas
FCanvasassociated with a bridge.
Configure the bridge logging system to display log traces here in the notebook, a convenience that alleviates the need to open the browser console.
NOTE: BCanvas could be a Bridge plugin, but it’s a separate widget instead in order to use another comm channels. (must check if we gain something by having different Comm channels, in the end ZMQD uses one websocket under the hood, I think)
get_bcanvas
get_bcanvas (height:int=200, elid:str='', **kwargs)
cleanupwidgets('cnv')
__bcanvas__ = None
cnv = get_bcanvas(timeout=DEBUG(2, 2))
# test_eq(cnv.loaded(), True) # in Lab something weird is happening herecnv.show()cnv.add('Why Universe, why?<br>')BLogger
FLoggersubclass used by Bridge.
BLogger
BLogger (msg=None, canvas:bridget.logger.Canvas|None=None, height:int=200, show:bool=True, history:bool=True, **kwargs)
Simple logger that displays messages in a scrollable div
lgr = BLogger('BLogger initialized')lgr.log('test')lgr.error('test')lgr.warn('test')lgr.log('bbbb')lgr.log('cccccc', True)lgr.show()lgr.log('ddddddddd')Log directly.
Bridge bootstrap
Progressive bridge construction: Boot → Messenger → Bridge (with plugins). Each layer adds functionality. Use get_bridge() to get the full-featured bridge.
BridgeBoot
Simply setup the bridge and connect logging.
cleanupwidgets('brd')
brd = BridgeBoot.create(show_logger=True, timeout=DEBUG(2, 2), sleep=0.2)
test_eq(brd.loaded(), True)Note create blocks, it waits for the widget to be loaded (or the default timeout, see BlockingMixin).
brd.error('Erred!')brd.update_logger_config(color='darkgoldenrod') # type: ignorebrd.log('my trea~~sssure')lgr now that the bridge is opened reflects both frontend and kernel log messages and uses the JS bridge logging functionality.
scr = Script('''
const { bridge } = await brdimport('./bridge.js');
bridge.logger.log('Hi from JS land');
''', type='module')
display(HTML(scr))
time.sleep(0.1)
clear_output()Log from JS land.
@FC.delegates(BridgeBoot, keep=True) # type: ignore
def get_bridge(logger:NBLogger|None=None, show_logger:bool=False, **kwargs) -> BridgeBoot:
if not __brd__:
timeout, sleep = kwargs.pop('timeout', 2), kwargs.pop('sleep', 0.2)
brd = BridgeBoot.create(logger=logger, show_logger=show_logger, timeout=timeout, sleep=sleep, **kwargs)
return brd
assert __brd__ is not None
brd:BridgeBoot = __brd__
if logger: brd.logger = logger
if show_logger: brd.logger.show()
return brdbrd = get_bridge(show_logger=True)
test_is(get_bridge(), brd)brd.close()
test_is(__brd__ is None, True)close() disconnect the widget and remove the widget model from the JS bridge, so it can no longer communicate with the Python side. Besides that, it has no effect on bcanvas, or the modules imported with brdimport. Create a new bridge to reconnect.
We need to use AnyWidget to setup the JS bridge, a class. The python side of the bridge should be created once per notebook, no sense to have several bridges around.
Given the nearly impossibility of creating class singletons in Python even with metaclasses, in previous refactorizations, I’ve used SingletonConfigurable a-la-getipython. It works very well but it uglify the code.
Anyway, given we’re-all-adults-here, you should always use get_bridge to access the bridge instance.
brd = get_bridge(show_logger=True)Messages from the beyond
handle_message
class _A:
ctx_names={'A'}
def __init__(self): self.forward = _B()
def on_info(self, *args, **kwargs):
print('A', 'info', f"{args=}", f"{kwargs=}")
def _msg_fwrdr(self, *args, ctx:str, kind:str, **kwargs):
if ctx == 'B':
handle_message(self.forward, *args, ctx=ctx, kind=kind, **kwargs)
else:
print(f"{ctx=} Not forwarded")
class _B:
ctx_names={'B'}
def on_info(self, *args, **kwargs):
print('B', 'info', f"{args=}", f"{kwargs=}")
a = _A()
test_stdout(
lambda: handle_message(a, 'hello', ctx='A', kind='info', info='initialized'),
"A info args=('hello',) kwargs={'info': 'initialized'}\nctx='A' Not forwarded")
test_stdout(
lambda: handle_message(a, 'other', ctx='B', kind='info', info='forwarded'),
"B info args=('other',) kwargs={'info': 'forwarded'}")BridgeMessenger
A bridge that can receive messages from JS land.
BridgeBoot just loads the JS bridge and setup logging. Here we set the basis for more powerful messaging to and fro JS.
@FC.delegates(BridgeMessenger, keep=True) # type: ignore
def get_bridge(logger:NBLogger|None=None, show_logger:bool=False, **kwargs):
if not __brd__:
timeout, sleep = kwargs.pop('timeout', 3), kwargs.pop('sleep', 0.2)
brd = BridgeMessenger.create(logger=logger, show_logger=show_logger, timeout=timeout, sleep=sleep, **kwargs)
return brd
assert __brd__ is not None
brd = __brd__
if logger: brd.logger = logger
if not logger and show_logger: brd.logger.show()
return brdcleanupwidgets('brd')
brd = get_bridge(show_logger=True, timeout=DEBUG(2, 2))brd.debug_enabled('loader', enabled=True)brd.send(brd.msg(ctx='brd', cmd='echo', args='test'))msg = dict(ctx='loader', cmd='load', args={'confetti': 'https://esm.sh/canvas-confetti@1.6'}, timeout=DEBUG(1))
brd.send(msg)observer_loader = anysource('''
const { getObserverManager } = await brdimport('./observer.js');
console.log(getObserverManager)
''')
brd.send(dict(ctx='loader', cmd='load', args={'get_observer': observer_loader}, reload=True), timeout=DEBUG(1))({'ctx': 'loader',
'kind': 'load',
'success': ['get_observer'],
'failed': [],
'msg_id': 'msg-8'},
[])
src = '''
const res = await brdimport('https://a.com/b/c.js');
console.log(res)
'''
brd.send(dict(ctx='loader', cmd='load', args={'willfail': src}, reload=True), timeout=DEBUG(1))({'ctx': 'loader',
'kind': 'load',
'success': [],
'failed': [{'name': 'willfail',
'error': 'Failed to fetch dynamically imported module: https://a.com/b/c.js'}],
'msg_id': 'msg-9'},
[])
anysource doesn’t transform its arguments. Use bundled (i.e., Bundle.__call__(..., transform=True), the default), instead.
You can disabled JS transform completely with bridge.cfg.bundle_cfg.rewrite_imports.
src = bundled('''
import confetti from 'https://esm.sh/canvas-confetti@1.6';
console.log(confetti)
''')(transform=False)
brd.send(dict(ctx='loader', cmd='load', args={'load_confetti': src}), timeout=DEBUG(1))({'ctx': 'loader',
'kind': 'load',
'success': ['load_confetti'],
'failed': [],
'msg_id': 'msg-10'},
[])
Check the JS console network tab, we imported the confetti module previously.
src = anysource('''
const { confetti } = await brdimport('https://esm.sh/canvas-confetti@1.6');
console.log(confetti)
''')
brd.send(dict(ctx='loader', cmd='load', args={'load_confetti': src}, reload=True), timeout=DEBUG(1))({'ctx': 'loader',
'kind': 'load',
'success': ['load_confetti'],
'failed': [],
'msg_id': 'msg-11'},
[])
brdimport doesn’t re-import confetti too.
Note the reload argument of the send method. bridge caches the modules by name and URL, and we’re reusing load_confetti name.
Use cache=False if you don’t want bridge caching the module.
Linking elements loader
script(and other links) loader for notebooks.
FastHTML way of loading head elements is fine with standard web apps, head links are evaluated in order (unless they have async or defer attributes) if present in the HTML source.
In notebooks, we need to load those head elements dinamically in order (in head, body or anywhere of the front-end page). And we want to load fasthtml.js (and htmx.js because why not) as soon as possible, so we can use them in the same cell to define our JS extensions.
Assuming we’ve already loaded fasthtml.js, this is a possible solution:
Links
Links (*fts:fastcore.xml.FT)
load_links
load_links (*fts:fastcore.xml.FT, feedback:str='')
brd.logger.show()scr = Script(notdebug('''
const { bridge } = await brdimport('./bridge.js');
bridge.logger.log('silly script');
'''), id='silly-script', type='module')
load_links(scr)load_links(scr) # see console, script was not loaded twiceload_links can be used to load any link element in the front-end in order (if bridge is active). It will auto delete the script after links are loaded if feedback is None so the link won’t be reflected in the .ipynb file and loaded automatically on page open.
Unfortunately, in some Jupyter environments like VSCode, this only works if the cell is visible in the screen and is run interactively, not with all below or all above. VSCode only renders outputs that are visible. For an alternative, see Loader below.
B('a')<b>a</b>BridgePlugin
BridgePlugin
BridgePlugin (ctx:str='', src:str|pathlib.Path='', bridge=None)
Inherit from this to have all attr accesses in self._xtra passed down to self.default
plg = BridgePlugin('test', 'a=10')
test_eq(plg.ctx_name, 'test')
test_eq(plg.ctx_names, {'test'})
test_eq(plg.src, 'a=10')Bridge
BridgeWidget + plugins to extend Bridge functionality in Python and/or JavaScript.
BridgeWidget contains the core functionality, logging and JS loading. All other stuff in this project will be developed with plugins.
def get_bridge(*plugins:BridgePlugin, kwplugins:dict[str, str]|None=None,
logger:NBLogger|None=None, show_logger:bool=False, **kwargs):
"Get the bridge, creating it if not found."
if not __brd__:
timeout, sleep = kwargs.pop('timeout', 2), kwargs.pop('sleep', 0.2)
brd = Bridge.create(*plugins, kwplugins=kwplugins, logger=logger, show_logger=show_logger,
timeout=timeout, sleep=sleep, **kwargs)
return brd
assert __brd__ is not None
brd = __brd__
if logger: brd.logger = logger
if not logger and show_logger: brd.logger.show()
return brdcleanupwidgets('brd')
brd = get_bridge(show_logger=True, timeout=DEBUG(2, 2))
test_eq(brd.plugins.keys(), set(('loader', 'htmx', 'fasthtmljs')))brd.add_plugins(badp := BridgePlugin('badp', '''export default function badp(bridge) { a = 1/0 }'''))
blocks(lambda: badp.is_initialized != None, 1)
test_is(brd.badp.is_initialized, False)InspectPlugin
class InspectPlugin(BridgePlugin):
src = '''
function inspect(msg) {
const {ctx, kind} = msg;
if (kind === 'echo') {
bridge.logger.log('echo', msg);
setTimeout(() => {
bridge.model.send({ ctx: ctx, kind: 'echo', msg: msg, msg_id: msg?.msg_id })
}, 100);
return;
}
bridge.model.send({ ctx: 'inspect', kind: 'inspect', msg: msg, msg_id: msg?.msg_id })
}
export default function initializeInspect(bridge) {
bridge.on('inspect', inspect);
return () => bridge.off('inspect');
}
'''
ctx_name = 'inspect'
def on_inspect(self, *args, msg:Any, tracker:Any, **kwargs):
self.log(f"{self.__class__.__name__} inspect: {msg=} {tracker=}")brd.logger.show(clear=True)brd.add_plugins(insp := InspectPlugin())
blocks(lambda: insp.is_initialized is not None, 3)
test_is('inspect' in brd.plugins, True)add_plugin does not blocks. If needed, use blocks or blocking to ensure the plugin is loaded.
# content, _ = await insp.asend(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1})) # asend not working in Lab
content, _ = insp.send(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1}), timeout=DEBUG(1))
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})content, _ = insp.send(msg := insp.msg({'tracker': 'test'}, ctx='inspect', kind='echo', data={'b': 2}), timeout=DEBUG(1))
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})insp.src = '''
const logger = bridge.logger.create({ ns: 'inspect', color: 'green' });
function inspect(msg) {
const {ctx, kind} = msg;
if (kind === 'echo') {
logger.log('echo', msg);
setTimeout(() => {
bridge.model.send({ ctx: ctx, kind: 'echo', msg: msg, msg_id: msg?.msg_id })
}, 100);
return;
}
bridge.model.send({ ctx: 'inspect', kind: 'inspect', msg: msg, msg_id: msg?.msg_id })
}
export default async function initializeInspect(bridge) {
bridge.on('inspect', inspect);
return () => { bridge.off('inspect'); logger.close(); }
}
'''brd.logger.show(clear=True)brd.add_plugins(insp)insp.log("Look Ma', now greeny!")content, _ = insp.send(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1}), timeout=2)
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})We can re-load any plugin by calling addPlugins again. See above insp now uses a new logger.
In the examples folder, there’s an inspect plugin more fully developed.
Loader
Convenience python-side plugin for loading scripts and ESMs.
brd.logger.show(clear=True)loader = Loader(dict(
test=Script('// debugger;\nconsole.log("test")', id='test-script'),
test2=Script('// debugger;\nconsole.log(a)', id='test-script2')
))
brd.add_plugins(loader)
test_is(brd.loader.loading, True)
test_is(brd.loader, loader)loader.load_links({
'test3': Script('// debugger;\nbridge.logger.log("test3")', id='test-script3'),
'test4': Script('// debugger;\nbridge.logger.log(a)', id='test-script4')
})loader.load({'htmx1': '''
// debugger;
import htmx from "https://unpkg.com/htmx.org@next/dist/htmx.esm.js";
console.log(htmx);
''',
'htmx2': '''
// debugger;
import htmx from "https://unpkg.com/htmx.org@next/dist/XXXX.esm.js";
console.log(htmx);
'''})blocks(lambda: loader.loaded('htmx1'), 2, show=_show) # needed when running all above/below cells
test_eq(loader.loaded().keys(), set(('test', 'test3', 'htmx1')))._.
test_eq(brd.loader.loaded('htmx1'), True)
test_eq(brd.loader.loaded('nah'), False)Loader can be used to execute any JS code (as an EcmaScript module). Note that unlike IPython Javacript, there’s no output, the code won’t run on opening the notebook until explicitly running the cell.
brd.logger.show()display(HTML(
'<button type="button" onclick="const canvas=document.getElementById(\'my-canvas\');\ncanvas.confetti({spread:70, particleCount:100, origin: { y: 1 }})">Fire!</button><br>'
'<canvas id="my-canvas" width="1000px" height="200px"></canvas>'
))
time.sleep(0.5)
loader.load({'confetti': '''
import confetti from "https://esm.sh/canvas-confetti@1.6";
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
const canvas = document.getElementById('my-canvas');
canvas.confetti = canvas.confetti || confetti.create(canvas, { resize: true });
'''}, reload=True)loader.load({'beep': '''
// debugger;
export function beep() {
var snd = new Audio("data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
snd.play();
}
window.$beep = beep;
'''})
display(HTML('<button type="button" onclick="window.$beep()">Beep!</button>'))HTMX plugin
HTMXPlugin
HTMXPlugin (ctx:str='', src:str|pathlib.Path='', bridge=None)
Inherit from this to have all attr accesses in self._xtra passed down to self.default
brd.logger.show(clear=True)brd.add_plugins(htmxp := HTMXPlugin())
blocks(lambda: htmxp.is_initialized is not None, 2, show=_show) # needed when running all above/below cells._.
True
htmxp.setup()ObserverManager
MutationObservermanager forbridget
bundled(observer_js)(debugger=DEBUG(), ts=True);brd.logger.show(clear=True)observer_scr = Script(bundled('''
import { getObserverManager } from './observer.js';
getObserverManager();
''')(),
type='module', id='brd-get-observer-manager')loader.load_links({'get_observer': observer_scr}) # load_links is asyncObserverManager is a normal ES module, just import it. But for local development in notebooks, or until we register it, we can use the loader or whatever of the many methods we now have to load JS code..
loader.loaded(){'test': script(('// debugger;\nconsole.log("test")',),{'id': 'test-script'}),
'test3': script(('// debugger;\nbridge.logger.log("test3")',),{'id': 'test-script3'}),
'htmx1': None,
'confetti': None,
'beep': None,
'get_observer': script(('\nconst {getObserverManager} = await brdimport("./observer.js");\ngetObserverManager();\n',),{'type': 'module', 'id': 'brd-get-observer-manager'})}
brd.logger.show()brd.loader.brdimport('./observer.js')
blocks(lambda: loader.loaded('./observer.js'), 2, show=_show) # needed when running all above/below cells._.
True
test_eq(loader.loaded('./observer.js'), True)We can also use with Python the equivalent to ES5 relative import declaration (don’t forget to bundled your source or use brdimport directly).
Front-end:
import { getObserverManager } from './observer.js';
const observer= getObserverManager();Kernel:
get_bridge().loader.brdimport('./observer.js')observer_plugin = anysource('''
export default async function initializeObserverPlugin(bridge) {
const { getObserverManager } = await brdimport('./observer.js');
return [null, { getObserverManager }]
}
''')or:
brd.logger.show(clear=True)brd.add_plugins(kwplugins={'observer':observer_plugin})[*brd.plugins.keys()]['loader', 'htmx', 'fasthtmljs', 'badp', 'inspect', 'observer']
We could also add observerManager as a plugin.
brd-mark
Bridge plugin that defines a custom element that adds data- attributes to its parent and remove itself. It also processes the parent with htmx.
brdmark_plugin = bundled(brdmark_js)()#(debugger=DEBUG(), ts=True)We could load brd_mark directly, but as it depends on the bridge (for logging), better to load it as a plugin.
brd.logger.show()brd.add_plugins(kwplugins={'brd_mark':brdmark_plugin})marker = 'aaaa<brd-mark id="marker-123">'
display(HTML(marker))print('asdf\nwerwqert')asdf
werwqert
display(HTML('<div class="bridge">aaaa</div><brd-mark id="marker-1234"></brd-mark>'))
display(HTML('<div class="bridge">bbbb</div><brd-mark id="marker-12345"></brd-mark>'))HTML(Brd_Mark(id=new_id()))get_bridge
show_summary
show_summary (brd:__main__.Bridge)
brd.logger.show()brd.loader.load_links({
'surreal':bridge_scripts()['surreal'],
'css_scope_inline':bridge_scripts()['css_scope_inline'],
})get_bridge
get_bridge (logger:NBLogger|None=None, show_logger:bool=False, lnks:dict[str,FT]|None=None, esms:dict[str,str|Path]|None=None, plugins:Sequence[BridgePlugin]|None=None, kwplugins:dict[str,str]|None=None, wait:int=0, summary:bool=False, factory:Callable[...,Any]|None=None, timeout:float=10, sleep:float=0.2, n:int=10, show:Callable[[bool],None]|None=None, **kwargs)
| Type | Default | Details | |
|---|---|---|---|
| logger | NBLogger | None | None | |
| show_logger | bool | False | |
| lnks | dict[str, FT] | None | None | |
| esms | dict[str, str | Path] | None | None | |
| plugins | SequenceBridgePlugin | None | None | |
| kwplugins | dict[str, str] | None | None | |
| wait | int | 0 | seconds to wait for plugins/links/modules to load |
| summary | bool | False | |
| factory | Callable[…, Any] | None | None | |
| timeout | float | 10 | |
| sleep | float | 0.2 | |
| n | int | 10 | |
| show | Callable[[bool], None] | None | None | |
| kwargs | VAR_KEYWORD |
nbdev.show_doc(get_bridge)get_bridge
get_bridge (logger:NBLogger|None=None, show_logger:bool=False, lnks:dict[str,FT]|None=None, esms:dict[str,str|Path]|None=None, plugins:Sequence[BridgePlugin]|None=None, kwplugins:dict[str,str]|None=None, wait:int=0, summary:bool=False, factory:Callable[...,Any]|None=None, timeout:float=10, sleep:float=0.2, n:int=10, show:Callable[[bool],None]|None=None, **kwargs)
| Type | Default | Details | |
|---|---|---|---|
| logger | NBLogger | None | None | |
| show_logger | bool | False | |
| lnks | dict[str, FT] | None | None | |
| esms | dict[str, str | Path] | None | None | |
| plugins | SequenceBridgePlugin | None | None | |
| kwplugins | dict[str, str] | None | None | |
| wait | int | 0 | seconds to wait for plugins/links/modules to load |
| summary | bool | False | |
| factory | Callable[…, Any] | None | None | |
| timeout | float | 10 | |
| sleep | float | 0.2 | |
| n | int | 10 | |
| show | Callable[[bool], None] | None | None | |
| kwargs | VAR_KEYWORD |
test_is(get_bridge(show_logger=True), brd)brd.close()
test_is(__brd__, None)brd.logger.clear_log()
with bridge_cfg(bootstrap=True):
brd = get_bridge(show_logger=True, wait=5, summary=True)Loaded scripts
loader htmx fasthtmljs observer brd_mark <script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script> <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
html = '''
<div>💩 👻 No style.</div>
<div>
<style> /* Simple example. */
me { margin: 20px; }
me div { font-size: 5rem; }
</style>
<div>👻</div>
</div>
'''
display(HTML(html))Bridge automatically loads some JavaScript libraries:
- HTMX
- FastHTML core scripts
- Awesome gnat’s Scope and Surreal scripts
When importing bridge, if bridge_cfg.bootstrap is True or there is an environment variable BRIDGET_BOOTSTRAP set to a true-ish value, it will automatically create a bridge.