Welcome to #MindPage! <<bounce('◄ tap!')>>

#_pin #_bounce

<<evallink(_this,'google()',icons.google)>>
<<evallink(_this,'tweet()',icons.twitter)>>
<<icons.twitter>>
<<icons.news>>
<<icons.youtube>>
<<icons.covid>>
<<icons.covid_ctp>>

// image macros for menu links above
const s = 24 // use 24px icons
const img_favicon = (d) => img(`https://${d}/favicon.ico`, s)
const img_touch = (d) => img(`https://${d}/apple-touch-icon.png`, s)
const img_db = (n) => img(`https://www.dropbox.com/s/${n}?dl=0`, s)
const img_wp = (n) => img('https://upload.wikimedia.org/wikipedia/'+n, s)
const icons = {
  // google:    img_favicon('google.com'),
  google:    img_wp(escape('commons/c/c1/Google_"G"_logo.svg')),
  cnn:       img_db('j1vx346nq7x0ujw/cnn.png'),
  news:      img_db('di7uv2b5d2lot26/apple_news.png'),
  twitter:   img_db('bsgvkbn8gszx79x/twitter.png'),
  youtube:   img_db('6ox8qfjpu1bn6a5/youtube.png'),
  covid:     img_db('0fez9jgzueiosdf/covid.png'),
  covid_ctp: img_db('em0t8h940oi55ka/covid_ctp.png')
}
const urls = {
  covid: 'https://www.worldometers.info/coronavirus/country/us/',
  covid_ctp: 'https://covidtracking.com/data/charts/us-all-key-metrics'
}
// functions used in menu links above
function google() {
  const q = MindBox.get().trim(); MindBox.clear()
  window.open("https://google.com/search?q="+encodeURIComponent(q))
}
function tweet() {
  const t = MindBox.get().trim(); MindBox.clear()
  // for web twitter: https://twitter.com/intent/tweet?text=...
  location.href="twitter://post?message="+encodeURIComponent(t)
}

#_pin/dot/1 #_menu

#items/naming/nested names have the form #…/name.

#Features are enabled by certain special tags: #/log #/_pin #/_menu #/_context #/_async #/_init #/_welcome #/_listen #/_autorun #/_autodep #/_style #/_spell #/_debug.

#Features/_autodep tag enables item and all descendants to automatically become first dependency on their children, thus forming a dependency chain/tree without having to use repetitive hidden tags.

#Shortcuts are listed below using #key_symbols. Most shortcuts are not available on virtual keyboards due to modifiers that are either missing (e.g. ) or inconsistent (e.g. ). Equivalent functionality should be available using buttons (e.g. create and save buttons for #⇧⏎). Please report issues to support@mind.page.

Keys Item Editor MindBox Window
⇧⏎ Save Create¹ Create
⌘S Save⁹ Create Create
⌘⏎ Save+Run Create+Run Create+Run
⌃⏎ Save+Run Create+Run Create+Run
⌥⌘⁶⏎ Save+Run Create²+Run Create²+Run
⌥⏎ Run+Save⁹ Create Create
Cancel Cancel Clear⁸
Clear
⇧⌫ Clear Clear
⌘⌫ Delete Clear Clear
^⌫ Delete Clear Clear
Indent Indent MindBox⁷
⇧⇥ Unindent Unindent MindBox
⌘/ (Un)Comment (Un)Comment MindBox
⇧⌘I Add Image Add Image Add Image
⇧⌘S Resume³ Resume Resume
⇧⌘⏎ Resume Create Resume
⇧⌃⏎ Resume Create Resume
⇧⌥⌘⁷⏎ Resume Save Resume
⌘↑ Edit Prev History Prev⁴ MindBox
⌘↓ Edit Next History Next⁵ MindBox
¹ Create & save new item, continue editing
² Create & save new item (stop editing)
³ Resume last edit (after save)
Previous in MindBox history
Next in MindBox history, or edit top item
Any 2+ modifiers (⌃⌥⌘) allowed
Focus keyboard on #MindBox
Focus on #MindBox if already clear
Save item, continue editing

#Features/_style tag designates the item as a style item such that its css_style block is automatically installed/updated on the page (under &lt;head>&lt;style …>). In addition, any dependents of the style item are automatically styled (on their &lt;div class="item …">) using a special CSS class name derived from the style item name as in dep_&lt;name>. Any invalid characters (outside [A-Za-z0-9_]) are replaced with _.

#MindPage/core/functions are:

#video macro embeds a YouTube video into item.

const video_options = {
  autoplay: false,
  show_related: false // false may still show videos from same channel
}
function video(video_id, options) {
  options = _.merge(video_options, options);
  return html(_item('$id').read('html') // from html block (edit to see)
          .replaceAll('%video_id', video_id)
          .replaceAll('%autoplay', options.autoplay ? '1':'0')
          .replaceAll('%rel', options.show_related ? '1' : '0'))
}
// optional toggle that can show/hide videos in item
const video_toggle = _item('$id').read('html_toggle') // see below
  .replaceAll('%id', _that.id)
// optional link to video on youtube
const video_link = (video_id) =>
  `[↗︎ open](https://www.youtube.com/video/${video_id}?rel=0)`
// optional link to replace embedded video in same item
const video_switch = (video_id, text) =>
  _item('$id').read('html_switch') // see below
    .replaceAll('%id', _that.id)
    .replaceAll('%video_id', video_id)
    .replaceAll('%text', text)
&lt;style>
  /* .video { display: none } */
  /* .video-toggle-hide { display: none } */
  /* .video-toggle-show { display: inline } */
  /* .item.show-videos .video { display: block } */
  /* .item.show-videos .video-toggle-hide { display: inline } */
  /* .item.show-videos .video-toggle-show { display: none } */
  .video-toggle-show { display: none }
  .hide-videos .item:not(.show-videos) .video { display: none }
  .hide-videos .item:not(.show-videos) .video-toggle-hide { display: none }
  .hide-videos .item:not(.show-videos) .video-toggle-show { display: inline }
  .video-toggle-show, .video-toggle-hide { float: right }
  .video-toggle-hide { border-radius: 4px 4px 0 0 !important; background: black !important }
  /* NOTE: 2px vertical padding allows touching video below &lt;ul> */
  .video-toggle-show, .video-toggle-hide { padding: 2px 8px !important; }
  
&lt;/style>
&lt;div id="$cid" class="video" style="position:relative;width:100%;height:0;padding-bottom:56.25%;background:#171717;border-radius: 4px 0 4px 4px;overflow:hidden">
&lt;iframe class="video" src="https://www.youtube.com/embed/%video_id?autoplay=%autoplay&rel=%rel&playsinline=1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;border-radius:4px 0 4px 4px;overflow:hidden">&lt;/iframe>
&lt;/div>
&lt;a href="javascript:_item('%id').elem.classList.add('hide-videos');_item('%id').elem.querySelector('.item').classList.remove('show-videos')" onclick="event.stopPropagation()" class="video-toggle-hide">▲ hide video&lt;/a> &lt;a href="javascript:_item('%id').elem.classList.remove('hide-videos');_item('%id').elem.querySelector('.item').classList.add('show-videos')" onclick="event.stopPropagation()" class="video-toggle-show">▼ show video&lt;/a>
&lt;a href="javascript:_item('%id').elem.querySelector('iframe').contentWindow.location.replace(_item('%id').elem.querySelector('iframe').src.replace(/(embed\/).+?(\?|$)/,'$1%video_id$2'));_item('%id').elem.classList.remove('hide-videos');_item('%id').elem.querySelector('.item').classList.add('show-videos')" onclick="event.stopPropagation()">%text&lt;/a>

#gapi/examples/gmail is an example for Gmail API. Quick Start Guide explains how to enable in console and create credentials for access.

const scope = 'https://www.googleapis.com/auth/gmail.readonly'
const doc = 'https://gmail.googleapis.com/$discovery/rest?version=v1'
await gapi.client._auth({ scope: scope, discoveryDocs: doc })
await gapi.client._try(async ({gmail})=>{
  // get list of emails in inbox (max 1 to print top email only)
  // see https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
  let messages = (await gmail.users.messages.list({
    userId:'me', labelIds:['INBOX'], maxResults: 1 }))?.result?.messages ?? []
  _this.log(`found ${messages?.length} messages`)
  if (!messages?.length) return _this.remove('_output') // no emails, stop
  // retrieve email contents
  messages = await Promise.all(messages.map(msg=>
    gmail.users.messages.get({ userId:'me', id:msg.id })))
  _this.log(`retrieved ${messages.length} messages`)
  return summarize_email(messages[0].result)
})
function summarize_email(email) {
  const subject = email.payload.headers.find(h=>h.name=='Subject')
    ?.value ?? '(missing subject)'
  let parts = (email.payload.parts ?? []).map(p=>p.parts ?? p).flat()
  let body = email.payload.body.data ??
    parts.find(p=>p.mimeType=='text/plain')?.body.data
  if (body) {
    // see https://stackoverflow.com/a/28622096 for body decoding logic
    body = _decode(body.replace(/-/g, '+').replace(/_/g, '/'), 'base64')
    body = body.replace(/\r\n/g, '\n').trim() // replace CRLF w/ LF only then trim
    // decode html entities using DOMParser, see https://stackoverflow.com/a/34064434
    body = new DOMParser().parseFromString(body, "text/html")
      .documentElement.textContent
    body = body.replace(/#/g, '\\#') // escape hashtags
    body = body.replace(/\s+/g, ' ') // normalize whitespace for snippet
  } else body = '(missing body)'
  const url = 'https://mail.google.com/mail/u/0/#inbox/' + email.id
  return [subject, _.truncate(body, {length:100}), url].join('\n\n')
}

#_gapi

#gapi/examples/profile is a basic example for Google People API.Quick Start Guide explains how to enable in console and create credentials for access.

const scope = 'https://www.googleapis.com/auth/userinfo.profile'
const doc = 'https://people.googleapis.com/$discovery/rest'
await gapi.client._auth({ scope: scope, discoveryDocs: doc })
await gapi.client._try(async ({people:{people}})=>{
  // fetch and output user name
  const response = await people.get({
    resourceName: 'people/me',
    personFields: 'names'
  })
  return "Your name is " + response.result.names[0]?.displayName
})

#_gapi

#commands/agenda command <<cmdlink('/agenda')>> creates an item listing upcoming events. Follow #gapi/examples/calendar to set up the API before initial use.

async function run() {
  const events = await fetch_events(5) // from #gapi/examples/calendar
  let agenda = events.length == 0 ? 'No upcoming events.' :
    ["|||", "|-|-", ...events.map((event) => {
      const ms = event.start.local.getTime() - Date.now()
      return '|' + [time_short(ms), event.summary].join('|')
    })].map((s)=>"> "+s).join('\n')
  return {
    text: "**Upcoming Events** from #commands/agenda\n" + agenda,
    // scroll to created item after dom update (so item.elem is non-null)
    init: item => _update_dom().then(()=>_scroll_to(item.elem.offsetTop)),
    edit: false
  }
}

#_gapi/examples/calendar #_macros

#gapi/examples/calendar is an example for Google Calendar API. Quick Start Guide explains how to enable in console and create credentials for access.

const events = await fetch_events() // see helper definition below
// output markdown table w/ event name, start date/time, calendar name
if (events.length == 0) _this.write('No upcoming events found.','_md_output')
else _this.write(['|||||', '|:-|:-|:-|:-|', ...events.map(e=>{
  const date = e.start.local.toLocaleDateString()
  const time = e.start.local.toLocaleTimeString()
  const cal = e.cal.summaryOverride || e.cal.summary || e.cal.id
  return '|' + [ _.truncate(e.summary), date, time, _.truncate(cal)].join(' | ')
})].join('\n'),'_md_output')
// helper to fetch events in all calendars, ordered by start time
async function fetch_events(limit_per_cal = 10) {
  const scope = 'https://www.googleapis.com/auth/calendar.readonly'
  const doc = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'
  await gapi.client._auth({ scope: scope, discoveryDocs: doc })
  return await gapi.client._try(async ({calendar})=>{
    // get all calendars
    const cals = (await calendar.calendarList.list()).result.items
    // read future events (up to 10 per calendar)
    let events = (await Promise.all(cals.map(async cal=>
      (await calendar.events.list({
        calendarId: cal.id, timeMin: new Date().toISOString(),
        showDeleted: false, singleEvents: true, 
        maxResults: limit_per_cal, orderBy: 'startTime'
      })).result.items.map(e=>Object.assign(e,{cal}))
    ))).flat()
    // compute event start-end times in local time zone, then sort by start time
    events.forEach(e=>{
      if (!e.start.dateTime) { // all-day event
        e.start.local = new Date(e.start.date + ' 00:00:00')
        e.end.local = new Date((e.end.date || e.start.date) + ' 00:00:00')
      } else {
        e.start.local = new Date(e.start.dateTime)
        e.end.local = new Date(e.end.dateTime || e.start.dateTime)
      }
    })
    return _.sortBy(events, 'start.local')
  })
}

#_gapi

#gapi is Google API client library. Here we load the core client library gapi.client and extend it with _auth([options]) and _try(func) to simplify authenticated access to specific scopes and credentials. See examples profile, calendar, and gmail.

// load GAPI, GIS (for auth), and GAPI client as needed
if (!window.gapi?.client || !window.google?.accounts?.oauth2) {
  await _load([
    window.gapi || 'https://apis.google.com/js/api.js',
    window.google?.accounts?.oauth2 || 'https://accounts.google.com/gsi/client'
  ])
  await new Promise((resolve, reject)=>gapi.load('client', 
    {callback: resolve, onerror: reject}))
}
const _gapi = _item('$id') // this item

gapi.client._auth = async function({
  scope, discoveryDocs, // required scope & discovery urls
  apiKey, clientId, clientSecret // optional auth info
}) {
  if (!scope) fatal('missing scope')
  if (!discoveryDocs) fatal('missing discoveryDocs')
  const scopes = scope.split(' ').map(s=>s.split('/').pop()).join(' ') // for logging
  discoveryDocs = [discoveryDocs].flat() // allow singleton doc
  
  try {
  
    apiKey ||= _gapi.global_store.api_key ||= await _modal({
      content:`${_this.name} needs your [API key](https://console.developers.google.com/apis/credentials)`,
      confirm:"Use API Key",
      cancel:"Cancel",
      input:""
    })
    if (!apiKey) throw new Error('missing api key')

    clientId ||= _gapi.global_store.client_id ||= await _modal({
      content:`${_this.name} needs your [Client ID](https://console.developers.google.com/apis/credentials)`,
      confirm:"Use Client ID",
      cancel:"Cancel",
      input:""
    })
    if (!clientId) throw new Error('missing client id')

    clientSecret ||= _gapi.global_store.client_secret ||= await _modal({
      content:`${_this.name} needs your [Client Secret](https://console.developers.google.com/apis/credentials)`,
      confirm:"Use Client Secret",
      cancel:"Cancel",
      input:""
    })
    if (!clientSecret) throw new Error('missing client secret')
   
    // init GAPI client (multiple init ok)
    await gapi.client.init({ apiKey, discoveryDocs })

    // get token if needed
    if (!_gapi._global_store._token_response ||
        !google.accounts.oauth2.hasGrantedAllScopes(
          _gapi._global_store._token_response, ...scope.split(' '))) {
      
      // get authorization code (with user consent) for "offline" (refreshable) token
      // note there are two cases where auth client fails to return/throw errors:
      // if popups are blocked, then it simply logs an error via console.error
      // if popup is closed w/o proceeding w/ auth, then it does nothing
      // to handle these, we set up console.error and a focus handler
      // see https://stackoverflow.com/q/72387245
      const _console_error = console.error // to be restored below
      let on_focus // to be removed as listener below
      let start = Date.now()
      _this.debug(`GAPI requesting authorization code for '${scopes}' ...`)
      const auth_code = await new Promise(async (resolve, reject) => {
        // set up console.error to handle popup errors (e.g. blocked by browser)
        console.error = (...args) => {
          if (args.join().includes('popup')) reject(new Error(args.join()))
          else _console_error(...args)
        }
        // set up focus handler to handle return to window without authorization
        on_focus = () => reject(new Error('failed to complete authorization'))
        addEventListener('focus', on_focus)
        try {
          // display modal to clarify next step and help enable popups (esp. on ios)
          const confirmed = await _modal({
            content:`${_this.name} needs your permission for '${scopes}'`,
            confirm:"Continue", 
            cancel:"Cancel",
          })
          if (!confirmed) return reject(new Error('cancelled by user'))          
          const auth_client = await google.accounts.oauth2.initCodeClient({            
            client_id: clientId, scope, ux_mode: 'popup',
            hint: _user.email, // help skip account selection
            select_account: false,
            callback: (resp) => {
              if (resp.error) return reject(resp.error)
              _this.log(`GAPI received authorization code for '${scopes}' in ${Date.now()-start}ms`)
              resolve(resp.code)
            }
          })
          auth_client.requestCode()
        } catch (e) {
          console.error(e)
          reject(resp)
        }
      }).finally(()=>{
        console.error = _console_error
        removeEventListener('focus', on_focus)
      })

      start = Date.now()
      _this.debug(`GAPI requesting access/refresh tokens for '${scopes}' ...`)
      // https://developers.google.com/identity/protocols/oauth2/web-server#httprest_3
      const resp = await fetch('https://oauth2.googleapis.com/token', {
        method:'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          code: auth_code,
          client_id: clientId,
          client_secret: clientSecret,
          grant_type: 'authorization_code',
          redirect_uri: 'https://' + location.host
        })
      }).then(r=>r.json())
      if (resp.error)
        throw new Error(`GAPI token request failed: ${JSON.stringify(resp)}`)
      _this.log(`GAPI received access/refresh tokens for '${scopes}' in ${Date.now()-start}ms`)
      _gapi.global_store._token_response = resp
    }
    gapi.client.setToken(_gapi.global_store._token_response)

    // set up gapi.client._refresh_token()
    // NOTE: access tokens are short-lived (~1h based on inspection of token response in console), so caller should call gapi.client._refresh_token() on 401 or 403 errors w/ status PERMISSION_DENIED (see getToken function in example at https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gapi-asyncawait)
    gapi.client._refresh_token ??= async () => {
      let start = Date.now()
      _this.debug(`GAPI requesting fresh access token ...`)
      // https://developers.google.com/identity/protocols/oauth2/web-server#httprest_7
      const resp = await fetch('https://oauth2.googleapis.com/token', {
        method:'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          client_id: clientId,
          client_secret: clientSecret,
          grant_type: 'refresh_token',
          refresh_token: _gapi.global_store._token_response.refresh_token
        })
      }).then(r=>r.json())
      if (resp.error)
        throw new Error(`GAPI token request failed: ${JSON.stringify(resp)}`)
      _this.log(`GAPI received new access token in ${Date.now()-start}ms`)
      // keep refresh token in _token_response
      // scopes are also maintained in _aggregate_ by google (server side)
      // this allows the client to maintain a single access token w/ all permissions
      resp.refresh_token = _gapi.global_store._token_response.refresh_token
      _gapi.global_store._token_response = resp
      gapi.client.setToken(resp)
    }

    // uncomment to test refresh
    // await gapi.client._refresh_token()
    
  } catch(e) {
    throw new Error(`GAPI init failed for '${scopes}': ${e}`)
  }
}

gapi.client._try = async function(func, tries = 0) {
  if (!gapi.client._refresh_token) fatal('gapi.client._auth is required before _try')
  try {
    return await func(gapi.client)
  } catch (e) {
    e = e.result?.error ?? e // dig into gapi error
    // if html is returned in e.body, use title as error message
    let title = e.body?.match(/&lt;title>(.+?)&lt;\/title>/)?.pop()
    if (title) {
      // if code block is found in body, append to title
      const code = e.body?.match(/&lt;code>(.+?)&lt;\/code>/)?.pop()
      if (code) title += ' (' + code + ')'
      e = new Error(title)
    }
    if (tries == 0) { // consider retry
      if (e.code == 401 || (e.code == 403 && e.status == 'PERMISSION_DENIED')) {
        // access token is missing, invalid, or expired, so retry after refresh
        _this.log(`GAPI retrying with fresh token after error ${e.code} (${e.status})`)
        await gapi.client._refresh_token()
        return await gapi.client._try(func, ++tries)
      }
    }
    _this.error(e)
  }
}

#_load #_async

#MindPage is a secure #private notebook that renders plain text #items into #text, #images, #code, #math, #charts, #graphs, #animations, or any other web content. MindPage is readily exportable, extensible, open source, and in #beta stage. See also intro by founder Olcan.
#_context

#animations can be generated using #macros/animation ...
<<animation(green_box, {autoplay:true})>>
<<animation(red_box, {start:.5, show_reset:true})>>
<<animation(explosion)>>
<<animation(explosion_gradient)>>

function green_box(ctx, t, w, h) {
  ctx.clearRect(0, 0, w, h)
  ctx.fillStyle = 'green'
  ctx.fillRect(t/3*w-25, (.5+.25*Math.sin(t*Math.PI))*h-25, 50, 50)
}
function red_box(ctx, t, w, h) {
  ctx.clearRect(0, 0, w, h)
  ctx.fillStyle = 'red'
  ctx.fillRect(t/3*w-25, (.5+.25*Math.sin(t*4*Math.PI))*h-25, 50, 50)
}
function explosion(ctx, t, w, h) {
  ctx.clearRect(0, 0, w, h)
  const circles = 50
  ctx.lineWidth = w/circles
  ctx.strokeStyle = 'white'
  for (let j=0; j&lt;circles; ++j) {
    const r = (j+0.5) / circles
    ctx.globalAlpha = Math.pow(
      Math.min(1, Math.max(0, 1 - Math.abs(t/3-r))), 10)
    ctx.beginPath();
    ctx.arc(w*.5, h*.5, r*w, 0, 2 * Math.PI);
    ctx.stroke();
  }
}
function explosion_gradient(ctx, t, w, h) {
  ctx.clearRect(0, 0, w, h)
  const stops = 50
  let grd = ctx.createRadialGradient(w*.5, h*.5, 0, w*.5, h*.5, w)
  for (let j=0; j&lt;stops; ++j) {
    const r = j/stops
    const a = Math.pow(Math.min(1, Math.max(0, 1 - Math.abs(t/3-r))), 10)
    grd.addColorStop(r, `rgba(255, 255, 255, ${a}`);
  }
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, w, h);  
}

... and can be GPU-accelerated using WebGL:
<<animation(webgl_draw,{init:webgl_init(_this.read('glsl_shader'))})>>

precision mediump float; 
uniform float t, w, h;
void main(void) {
  float x = gl_FragCoord.x / w;
  float y = 1.0 - gl_FragCoord.y / h;
  y = ((y - 0.5) * h / w) + 0.5; // rescale y to get circles
  float r = sqrt((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5));
  float a = pow(min(1.0, max(0.0, 1.0 - abs(t/3.0 - r))), 10.0);
  gl_FragColor = vec4(a); // premultiplied alpha reduces fringing
}

Also see examples #/brownian_motion and #/ornstein_uhlenbeck.
#_macros/animation #_webgl

#animations/ornstein_uhlenbeck

const sigma = 0.05     // diffusion rate (∝1/mass)
const theta = 0.1      // mean reversion rate
const tail_time = 5    // past length in seconds
const sim_time = 5     // future sim time (in secs, when paused)
const sim_count = 1000 // number of future simulations (when paused)
// const drift = (t) => ({x:0,y:0})
const drift = (t) => ({ // ∞-shaped periodic drift
  x: Math.cos(.5 * t * 2*Math.PI),
  y: 1.5 * Math.cos(t * 2*Math.PI)
})

<<animation(draw, {period:0, style:'width:500px;height:500px', reset:true, init})>>

let t0 = 0
let x = .5, y = .5 // start in center
let path = [[x,y]]
const max_points = tail_time * 60
function draw(ctx, t, w, h, {pause, t_end=0, offset=0, alpha=1}) {
  if (t > t0 + .1) { t0 = t; return } // ignore big time steps
  if (t &lt; t0) { t0 = 0; x = .5; y = .5; path = [[x,y]] } // reset
  // on pause, simulate future paths
  if (pause) {
    path = [[x,y]] // clear tail on pause (could be optional)
    const sim_start = Date.now()
    const x_sim = x, y_sim = y, t0_sim = t0, path_sim = path.slice()
    ctx.clearRect(0, 0, w, h)
    for (let i=0; i&lt;sim_count; ++i) {
      draw(ctx, t, w, h, {
        pause: false, 
        t_end: t + sim_time, 
        offset: i==0 ? 0 : path_sim.length-1, 
        alpha: 5/sim_count
      })
      x = x_sim; y = y_sim; t0 = t0_sim; path = path_sim.slice()
    }
    return {t} // rewind time to ignore simulation time
  }
  const pi = path.length - 1 // last point before drawing
  do {
    // update position, time, tail
    const W = jStat.normal(0,sigma*Math.sqrt(t-t0))
    const mu = drift(t)  
    x += W.sample() + (mu.x - theta*(x-.5)) * (t-t0)
    y += W.sample() + (mu.y - theta*(y-.5)) * (t-t0)
    path.push([x,y])
    t0 = t
    if (t_end&lt;=0) while (path.length > max_points) path.shift()
    else t += 1/60 // simulate at 60fps
  } while (t &lt; t_end)
  // draw tail 
  // do not clear canvas if simulating futures (t_end>0)
  if (t_end&lt;=0) ctx.clearRect(0, 0, w, h)
  ctx.strokeStyle = 'white'
  ctx.lineWidth = 3
  for (let i=offset; i&lt;path.length-1; ++i) {
    ctx.globalAlpha = i &lt; pi ? (i+1)/path.length : alpha
    ctx.beginPath()
    ctx.moveTo(path[i][0]*w, path[i][1]*h)
    ctx.lineTo(path[i+1][0]*w,path[i+1][1]*h)
    ctx.stroke()
  }
  // draw particle (at start of path if simulating futures)
  ctx.globalAlpha = 1  
  ctx.fillStyle = 'white'
  ctx.beginPath()
  if (t_end&lt;=0) ctx.arc(w*x, h*y, 10, 0, 2*Math.PI)
  else ctx.arc(w*path[pi][0], h*path[pi][1], 10, 0, 2*Math.PI)
  ctx.fill()
}
// initializer to ensure jStat is loaded
async function init(canvas) {
  await _load(window.jStat || 'https://cdn.jsdelivr.net/npm/jstat/dist/jstat.min.js')
  return canvas.getContext('2d')
}

#_macros/animation #_jStat

#animations/brownian_motion

const sigma = 0.05 // inverse mass of particle
const tail = 10    // past length in seconds
const drift = (t) => ({ // ∞-shaped periodic drift
  x: Math.cos(.5 * t * 2*Math.PI),
  y: 1.5 * Math.cos(t * 2*Math.PI)
})

<<animation(draw, {period:0, reset:true, init})>>

let t0 = 0
let x = .5, y = .5 // start in center
let path = [[x,y]]
const maxPoints = tail * 60
function draw(ctx, t, w, h) {
  if (t > t0 + .1) { t0 = t; return } // ignore big time steps
  if (t &lt; t0) { t0 = 0; x = .5; y = .5; path = [[x,y]] } // reset
  // update position, time, tail
  const W = jStat.normal(0,sigma*Math.sqrt(t-t0))
  const mu = drift(t)
  x += W.sample() + mu.x * (t-t0)
  y += W.sample() + mu.y * (t-t0)
  t0 = t
  path.push([x,y])
  while (path.length > maxPoints) path.shift()
  // draw tail
  ctx.clearRect(0, 0, w, h)
  ctx.strokeStyle = 'white'
  ctx.lineWidth = 3
  for (let i=0; i&lt;path.length-1; ++i) {
    ctx.globalAlpha = (i+1)/path.length
    ctx.beginPath()
    ctx.moveTo(path[i][0]*w, path[i][1]*h)
    ctx.lineTo(path[i+1][0]*w,path[i+1][1]*h)
    ctx.stroke()
  }
  // draw particle
  ctx.globalAlpha = 1  
  ctx.fillStyle = 'white'
  ctx.beginPath()
  ctx.arc(w*x, h*y, 10, 0, 2*Math.PI)
  ctx.fill()
}
// initializer to ensure jStat is loaded
async function init(canvas) {
  await _load(window.jStat || "https://cdn.jsdelivr.net/npm/jstat@latest/dist/jstat.min.js")
  return canvas.getContext('2d')
}

#_macros/animation #_jStat

#TensorFlow/examples/linear_regression is the official linear regression tutorial from TensorFlow CodeLab. We extend the example slightly with L2 regularization, early stopping criteria (to help avoid poor results), and support for WebAssembly (CPU), which can be faster than WebGL (GPU) on smaller problems.

const wasm = true // use WebAssembly (CPU) vs WebGL (GPU)
async function main() {

  // define and summarize model
  const model = tf.sequential()
  model.add(tf.layers.dense({
    inputShape: [1], units: 1, name:'linear_l2',
    kernelRegularizer: tf.regularizers.l2({l2:.01})
  }))
  render(tfvis.show.modelSummary,
    ['model', {name:'model', tab:_this.name}], model)

  // fetch and plot data
  let data = await fetch('https://storage.googleapis.com' +
    '/tfjs-tutorials/carsData.json')
  data = await data.json()
  data = data.map(car => ({
    mpg: car.Miles_per_Gallon, horsepower: car.Horsepower 
  })).filter(car => car.mpg && car.horsepower)
  _this.log(`fetched ${data.length} data points`)
  const obs = data.map(d => ({x:d.horsepower,y:d.mpg}))
  render(tfvis.render.scatterplot,
    ['data', {name: 'data', tab:_this.name}],
    {values:obs, series:['observed']},
    {xLabel:'horsepower',yLabel:'mpg',height:300})

  // convert data into tensors
  data = tf.tidy(()=>{ // auto-release temporary tensors
    tf.util.shuffle(data)
    const J = data.length  
    let inputJ1 = tf.tensor2d(data.map(d=>d.horsepower), [J,1])
    let labelJ1 = tf.tensor2d(data.map(d=>d.mpg), [J,1])
    const [input_min, input_max] = [inputJ1.min(), inputJ1.max()]
    const [label_min, label_max] = [labelJ1.min(), labelJ1.max()]
    inputJ1 = inputJ1.sub(input_min).div(input_max.sub(input_min))
    labelJ1 = labelJ1.sub(label_min).div(label_max.sub(label_min))
    return { inputs: inputJ1, labels: labelJ1,
             input_min, input_max, label_min, label_max } })
  
  // train model
  model.compile({
    optimizer: tf.train.sgd(.01),
    loss: tf.losses.meanSquaredError,      
    metrics: ['mse']
  })
  tfvis.visor().open() // open visor (sidebar)
  const start = Date.now()
  let history = []    
  await model.fit(data.inputs, data.labels, { 
    batchSize: 16, epochs: 100, shuffle: true,
    callbacks: [
      tf.callbacks.earlyStopping({monitor:'mse',
        minDelta:.0005, patience:5, verbose:1}),
      new tf.CustomCallback({
        onEpochEnd: (epoch, log) => {
          history.push(log)
          render(tfvis.show.history,
            ['history', { name: 'training', tab:_this.name }],
            history, ['mse'], {xLabel:'iteration',yLabel:'mse'})
        }
      })
    ]
  })
  _this.log(`training done in ${Date.now()-start}ms`)
  
  // generate predictions
  const [xK, predK] = tf.tidy(() => {
    let xK = tf.range(0,100).div(100)
    let predK1 = model.predict(xK.reshape([100, 1]))
    const {input_min, input_max, label_min, label_max} = data
    xK = xK.mul(input_max.sub(input_min)).add(input_min)
    predK1 = predK1.mul(label_max.sub(label_min)).add(label_min)
    return [xK.dataSync(), predK1.dataSync()]
  })
  // plot together with observed data
  const preds = Array.from(xK).map((x,k)=>({x,y:predK[k]}))
  render(tfvis.render.scatterplot,
    ['data', {name: 'data', tab:_this.name}],
    {values: [obs, preds], series:['observed','predicted']},
    {xLabel:'horsepower',yLabel:'mpg',height:300})
  
  // clean up
  tf.dispose(data)
  tf.dispose(model)  
}
await tf._run(main, wasm ? 'wasm' : 'webgl')

<<tfvis_container('model')>>
<<tfvis_container('data')>>
<<tfvis_container('history')>>
<<tfvis_visor_toggle>>
#_TensorFlow/vis

fetched 392 data points
Epoch 42: early stopping.
training done in 1325ms
tf._run took 1810ms using wasm

Videos

#_pin/1 #_video

#beta means actively developed but stable enough for general public use, now open to anyone with a Google account for authentication. A GitHub account is also recommended for technical users.

#commands/export command <<cmdlink('/export')>> downloads a MindPage.zip file containing all of your items as plain text files:

async function run(extension) {
  if (!extension) extension = "markdown"
  const items = _items()
  const modal = _modal({content: `Exporting ${items.length} items ...`});
  await _load([
    window.JSZip || 'https://cdn.jsdelivr.net/npm/jszip@3.6.0/dist/jszip.js',
    window.saveAs || 'https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js'
  ])
  let zip = new JSZip()
  let root = zip.folder('MindPage')
  await Promise.all(items.map(async (item, index)=>{
    let fname = (index+1).toString().padStart(6, '0')
    let text = item.text;
    const srcs = item.images() // private image srcs
    if (srcs.length > 0) {
      _modal_update(modal, {content: `Downloading ${srcs.length} images for item ${item.name} ...`})
      const blobs = await item.images({output:'blob'});
      let folder = root.folder(fname)
      blobs.forEach((blob, i) => {
        const img_fname = srcs[i]+'.'+mime2ext(blob.type)
        text = text.replaceAll(srcs[i], fname + "/" + img_fname)
        folder.file(img_fname, blob, {date:new Date(item.time)})
      })
    }
    root.file(fname+'.'+extension, text, {date:new Date(item.time)})
  }))
  _modal_update(modal, {content: `Exporting ${items.length} items ...`})
  saveAs(await zip.generateAsync({ type:'blob' }), `MindPage.zip`)
  _modal_close(modal)
}
// from https://stackoverflow.com/a/8801399
function mime2ext(type) {
  switch(type) {
    case "image/gif":  return "gif"
    case "image/jpeg": return "jpg"
    case "image/png":  return "png"
    case "image/tiff": return "tiff"
    case "image/vnd.wap.wbmp": return "wbmp"
    case "image/x-icon": return "ico"
    case "image/x-jng": return "jng"
    case "image/x-ms-bmp": return "bmp"
    case "image/svg+xml": return "svg"
    case "image/webp": return "webp"
    case "application/pdf": return "pdf"    
    default: throw new Error("unknown image type: "+type)
  }
}

#_async #_load

&lt;div id="time-$cid" style="color:#aaa">
&lt;div class="info" style="float:right">&lt;/div>
&lt;div class="time">&lt;/div>
&lt;style> #item .unit { font-size: 80%; color:#666; margin-right:5px } &lt;/style>
&lt;script>
const unit = (s) => `&lt;span class="unit">${s}&lt;/span>`
_this.dispatch_task('time', ()=>{
  let div = _this.elem?.querySelector("#time-$cid")
  if (!div) return // div not on page (e.g. item being edited)
  const date = new Date()
  const day_time = date.toLocaleString()
    .replace(",", " &nbsp; ").replace(/ +(AM|PM)/, (_,u)=>unit(u))
  const dow = date.toLocaleString(
    navigator.language, {weekday:'short'})    
  div.children[0].innerHTML = 
    _user.oldest_item_time_string.replace(/[mdh]/, (u)=>unit(u))
    + " &nbsp; " + Math.floor(_user.total_text_length/1024) + unit('KB')
  div.children[1].innerHTML = dow + "&nbsp; " + day_time
}, 0, 1000) // dispatch now and every second
&lt;/script>
&lt;/div>

#_pin/dot/0

#TensorFlow/examples/blazeface implements blazeface (paper) face detection demo. Run item to start. Tap video to stop.

const width = 300     // separate from underlying video image size
const target_fps = 20 // lower target can make page more responsive
&lt;!-- _cache_key that depends on $id only ensures same div is used across edits, and _skip_invalidation ensures same div is used across runs; otherwise each run invalidates cache and replaces div by default, which is usually appropriate but not here since we manipulate the div directly and replacing it between runs can reset messages or leak video streams preventing proper stopping of camera -->
&lt;div id="faces-$id" _cache_key="faces-$id" _skip_invalidation style="position:relative;min-height:30px;cursor:pointer">
&lt;video style="position:absolute" autoplay playsinline>&lt;/video>
&lt;canvas style="position:absolute">&lt;/canvas>
&lt;span style="position:absolute">Run item to start face detection.&lt;/span>
&lt;/div>
const span = _this.elem?.querySelector('#faces-$id span')
if (span) span.innerHTML = "Loading ..."
const tf_url = 'https://cdn.jsdelivr.net/npm/@tensorflow'
await _load([ window.tf || tf_url + '/tfjs/dist/tf.min.js',
              window.blazeface || tf_url + '-models/blazeface' ])
const model = await blazeface.load()
let video, canvas, lastUpdateTime
let height, scale, div_initialized = false
let updateCount = 0
let fps = 0
let lastFPSCalcTime = Date.now()
let start = Date.now()
const update = async () => {
  if (update != _this.store.update_task) return // cancelled
  const div = _this.elem?.querySelector('#faces-$id')
  // skip update if div is not on page (item hidden or editing)
  if (!div) { setTimeout(()=>requestAnimationFrame(update), 250); return }
  const span = div.querySelector('span')
  if (video != div.children[0]) { // need to start video
    video = div.children[0]
    span.innerHTML = "Initializing video ..."
    await _update_dom()
    let stream = await navigator.mediaDevices.getUserMedia({video:{facingMode:"user"}})
    video.srcObject = stream
    // video.play()  // fails on ios, but autoplay works
    // wait until video is loaded and metadata (width/height) available
    await new Promise((resolve)=>{video.onloadeddata = resolve})
    console.log("started video", video.videoWidth, video.videoHeight)
    span.innerHTML = "Detecting faces ..."
    scale = width / video.videoWidth
    height = scale * video.videoHeight
    video.width = width
    video.height = height
    // NOTE: div may no longer be available at this point (e.g. if item was hidden during video init), so we handle div init separately below
    div_initialized = false
  }
  if (!div_initialized) {
    // postpone if div is not on item (e.g. item hidden or editing)
    if (!div.closest('.item')) {
      setTimeout(()=>requestAnimationFrame(update), 250)
      return 
    }  
    canvas = div.children[1]
    canvas.width = width
    canvas.height = height
    div.style.width = width + 'px'
    div.style.height = height + 'px'
    div.onclick = (e) => {
      e.stopPropagation()
      tf.dispose(model) // release memory used for model
      _this.store.update_task = null // stop updates
      video.pause() // just in case
      video.srcObject.getTracks().forEach(track => track.stop())
      video.srcObject = null // release stream
      video.height = 0
      canvas.height = 0
      video = canvas = null
      div.style.height = 'auto'
      // div.style.zoom = '1'
      span.innerHTML = 'Run item to start face detection.'
      span.style.padding = 0
      span.style.background = 'transparent'
    }
    span.style.padding = '0 10px' // insert for video overlay
    span.style.background = 'rgba(0,0,0,.5)'
    div_initialized = true
    // start animation frames once dom is updated
    _update_dom().then(()=>requestAnimationFrame(update))
    return
  }
  // get face predictions from model
  const faces = await model.estimateFaces(
    video, false/*tensors*/, false /*flip*/, true /*annotate*/)
  if (update != _this.store.update_task) return // cancelled
  // draw predictions on canvas
  const ctx = canvas.getContext('2d')
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  if (lastUpdateTime && Date.now() - lastFPSCalcTime > 1000) {
    fps = Math.floor(1000 * updateCount/(Date.now()-lastFPSCalcTime))
    lastFPSCalcTime = Date.now()
    updateCount = 0
  }
  span.innerHTML = `${faces.length} face(s), ${fps} fps`
  for (let i = 0; i &lt; faces.length; i++) {
    const start = faces[i].topLeft
    const end = faces[i].bottomRight
    const size = [end[0] - start[0], end[1] - start[1]]
    ctx.lineWidth = 5;
    ctx.strokeStyle = 'rgb(0,255,0)'
    ctx.strokeRect(start[0]*scale, start[1]*scale, size[0]*scale, size[1]*scale)
    const landmarks = faces[i].landmarks
    ctx.fillStyle = 'rgb(255,0,0)'
    for (let j = 0; j &lt; landmarks.length; j++)
      ctx.fillRect(landmarks[j][0]*scale, landmarks[j][1]*scale, 5, 5)
  }
  updateCount++
  lastUpdateTime = Date.now()
  setTimeout(()=>{
    start = Date.now()
    requestAnimationFrame(update)
  }, Math.max(0, 1000/target_fps - (Date.now()-start)))
}
_this.store.update_task = update // cancels any others
await update() // start updating, await first update

#_TensorFlow

started video 640 480

#python/example #_python

print('hello world!')
hello world!

#typescript/example #_typescript

const hello : string = 'hello world';
console.log(hello) // log for _log block
return hello // also return for _output block
hello world
hello world

#webppl/examples/HMM implements #/model.

var J = 4;         // total steps
var x0 = 1         // initial state
var yJ = [0,0,0,0] // observations (y0 ignored)

<<bar_chart(_this.read('_output'),'height:200px',{posterior:'#d61',prior:'gray'})>>
<!--removed-->

var X_ = function(x) { Discrete({ps:x?[3,7]:[7,3]}) }
var Y_ = function(x) { Discrete({ps:x?[1,9]:[9,1]}) }
var xJ_ = function(J, _xJ, yJ) {
  if (_xJ.length == J) return _xJ // done!
  var x = sample(X_(_xJ[_xJ.length-1]))
  if (yJ && yJ.length > _xJ.length) observe(Y_(x), yJ[_xJ.length])
  xJ_(J, _xJ.concat(x), yJ)
}
var prior = function() { xJ_(J,[x0]) }
var posterior = function() { xJ_(J,[x0],yJ) }
webppl._summarize(_enumerate(posterior), _enumerate(prior))
[["x","[1,0,0,0]","[1,1,0,0]","[1,0,0,1]","[1,0,1,0]","[1,1,1,0]"],["posterior","0.83","0.09","0.04","0.02","0.01"],["prior","0.15","0.15","0.06","0.03","0.15"]]

<!--/removed-->
#_webppl #_macros/bar_chart #_macros

webppl took 364ms

#webppl/examples/hmm/model follows the HMM example from http://dippl.org/chapters/04-factorseq.html.

&lt;div id="dot-$id" class="dot" style="height:200px">
&lt;script>
var dot = `digraph {
  x0[shape=doublecircle]
  subgraph { x1->y1 }
  subgraph { x2->y2 }
  subgraph cluster { xj->yj; label="J-2"; class=stack }
  x0->x1->x2[constraint=false,minlen=3]
  x2->xj[constraint=false,minlen=5,style=dotted]
  y1,y2,yj[shape=doublecircle]
  x0[label="$x_0$"]
  x1[label="$x_1$"]
  x2[label="$x_2$"]
  xj[label="$x_j$"]
  y1[label="$y_1$"]
  y2[label="$y_2$"]
  yj[label="$y_j$"]
}`
graphviz._graph('#dot-$id', dot)
&lt;/script>
&lt;/div>

$x_j \mid (x_{j-1}=1) \sim \{ 0^.3, 1^.7 \}$
$x_j \mid (x_{j-1}=0) \sim \{ 0^.7, 1^.3 \}$
$y_j \mid (x_{j}=1) \sim \{ 0^.1, 1^.9 \}$
$y_j \mid (x_{j}=0) \sim \{ 0^.9, 1^.1 \}$
<<spacer(10)>>
$(x_j) \mid x_0,(y_j) \sim\ ?$
#_graphviz #_macros

#weight item charts your weight over time. You can enter your weight using #commands/weight or by editing this item (tap to see _data block).
<!--removed-->

3/2/2021  187.8
3/1/2021  188.7
2/26/2021  188.8
2/22/2021  189.2
2/19/2021  189.5
2/12/2021  188.9
2/8/2021  191.4
2/5/2021  192
1/29/2021  193.7
1/26/2021  195.2
1/25/2021  195.3
1/22/2021  195.3
1/20/2021  196.1
1/18/2021  197.2
1/13/2021  198.7

<!--/removed-->

&lt;div id="chart-$id" class="c3" style="height:120px"> &lt;script>
var data = _this.read('_data').split('\n').map((line)=>
  line.trim().replace(/["']?(\S+?)["']?[\s,:]+(\S+?)/, '"$1": $2')
).filter((t)=>t)
data = JSON.parse("{"+data.join(',')+"}")
c3._chart('#chart-$id', {
  data: { 
    x:'x', 
    columns: [ ['x', ..._.keys(data).map(Date.parse)],
               ['weight', ..._.values(data)] ] 
  },
  axis: {
    x: { type: 'timeseries' },
    y: { tick: { count:5, format: d3.format('.4r') } }
  },
  padding: { top: 10, right: 10 } // avoid cutting off tick labels
})
&lt;/script> &lt;/div>

#_c3

#TensorFlow/util defines various utility functions for #TensorFlow.

if (window.tf) { // skip if tf is not loaded yet
// _run invokes given function and checks for memory leaks
// wraps sync functions (but NOT async functions) in tf.tidy
// disposes globals both before and after invoking function
// sets (or switches) to specified backend if necessary
tf._run = async (func, backend = 'webgl') => {
  const start = Date.now()
  await tf._set_backend(backend)
  tf._release_memory()
  const _async = func.constructor.name == 'AsyncFunction'
  const out = _async ? await _this.attach(func()) :
                       tf.tidy(_this.attach(func))
  _this.log(`tf._run took ${Date.now()-start}ms ` +
    `using ${tf.getBackend()}`)
  tf._release_memory()
  await Promise.resolve(out).then(()=>
    tf._check_memory_for_leaks())
  return out
}
// sets backend, loading additional files as necessary
tf._set_backend = async (backend) => {
  if (tf.getBackend() == backend) return
  if (backend == 'wasm' && !tf.engine()?.registryFactory?.wasm) {
    await _load('https://cdn.jsdelivr.net/npm/@tensorflow' +
      '/tfjs-backend-wasm/dist/tf-backend-wasm.js')
  }
  await tf.setBackend(backend)
}
// releases any disposable global state for memory check
tf._release_memory = () => {
  tf.disposeVariables()
  // dispose store._tf in any animations
  _.values(window._animations).forEach(anim=>{
    if (!anim.store?._tf) return
    tf.dispose(anim.store._tf)
    anim.store._tf = {}
  })
}
// checks memory for possible leaks usign tf.memory()
tf._check_memory_for_leaks = () => {
  const mem = tf.memory()
  if (mem.unreliable) _this.warn('tf.memory unreliable')
  if (mem.numBytes > 0) _this.error('tf.memory.numBytes', mem.numBytes)
  if (mem.numBytesInGPU > 0) 
    _this.error('tf.memory.numBytesInGPU', mem.numBytesInGPU)
}
} // if (window.tf)

#_TensorFlow

#commands/js command /js code evaluates JavaScript code by creating a new item with a js_input block and then running it. Example: <<cmdlink('/js 2+2','/js 2+2','edit:false,run:true')>>

const run = (js) => ({text: "```js_input\n" + js + "\n```"})

#_macros #_MindBox

#images/macros can help simplify image syntax. You can use the generic macro or define your own macros for special urls or formatting.

<<img('icon.png')>> \&lt;&lt;img('…')>>
<<img('icon.png', 40)>> \&lt;&lt;img('…', 40)>>
<<img('icon.png', 32, 'auto', 'border:2px solid white; border-radius:8px')>> \&lt;&lt;img('…', 32, 'auto', 'border:2px solid white; border-radius:8px')>>

#images are added using +img button or ⇧⌘I shortcut.

#TensorFlow/vis is the official Visualization Library for #TensorFlow. Keyboard shortcuts ` (toggle visor) and ⇧` (=~, to toggle maximize once open) can be used to toggle the "visor" (sidebar) once library is loaded. Macro \&lt;&lt;visor_toggle>> (defined below) can also be used to render a toggle link in items. See basic #/example and linear_regression tutorial.

if (!window.tfvis) {
  const base = 'https://cdn.jsdelivr.net/npm/@tensorflow'
  const tfjs = base + '/tfjs/dist/tf.min.js'
  const tfjs_vis = base + '/tfjs-vis/dist/tfjs-vis.umd.min.js'
  if (window.tf) { // load tfjs-vis only
    await _load(tfjs_vis)
  } else { // load together with tfjs and then set backend
    await _load(tfjs, tfjs_vis)
    await tf.setBackend('webgl')
  }
  tfvis.visor().close() // initialize visor closed, minimized
  if (tfvis.visor().isFullscreen()) tfvis.visor().toggleFullscreen()
}

<!--removed-->

// convenience function that can render to multiple containers
// string targets are treated as container element ids, all other 
// targets are handled by tfvis (most common is a SurfaceInfo object)
const render = (tfvis_func, targets, ...args) => {
  return Promise.all([targets].flat().map((target) => {
    if (typeof target == 'string') { // interpret as container elem id
      // NOTE: missing containers can happen if the item is run before
      // first rendering, e.g. if it was typed/pasted into a new editor,
      // or if the item is hidden as it is being run; in these cases we
      // simply retry every 250ms for 3s and then log an error
      const start = Date.now()
      const try_render = () => {
        const elem = _this.elem?.querySelector('#'+target)        
        if (elem) _this.attach(tfvis_func(elem, ...args))
        else if (Date.now() - start &lt; 3000) setTimeout(try_render, 250)
        else console.error(`render: missing container '${target}'`)
      }
      try_render()
    } else return tfvis_func(target, ...args)
  }))
}
// set log filter for "Platform browser has already ..." warning
// so it does not get written into item (still logged in console)
// also include the WebGLv2 filter from #TensorFlow
_this.log_options.filter = (entry) =>
  !entry.text.startsWith('Platform browser has already') &&
  entry.text != 'Could not get context for WebGL version 2'
// install css for when tfvis is loaded
document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
  _item('$id').read('css_loaded') + "&lt;/style>") // see below
// set up css and any (global) macros
function _init() {
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
  // tfvis_visor_toggle macro for toggle link rendered in item
  window.tfvis_visor_toggle = `&lt;a _cached class="visor-toggle" title="toggle visor (~)" onclick="window.tfvis?.visor().toggle()">visor&lt;/a>`
  // tfvis_container macro for elements (divs) for rendering in item
  // custom id-only _cache_key ensures element is shared across edits
  // _skip_invalidation attribute ensures element is shared across runs
  window.tfvis_container = (id) => 
    `&lt;div class="tfjs-container" id="${id}" _cache_key="${id}-$id" ` + 
      `_skip_invalidation>&lt;/div>`
}
#tfjs-visor-container { color:black; position:absolute; height: 100%; top:0; right:0; z-index:5 }
#tfjs-visor-container button { border:0; cursor:pointer }
#tfjs-visor-container { filter: invert(100%) }
#tfjs-visor-container img { filter: invert(100%) }
.item .tfjs-container { filter: invert(100%); margin-top: 5px }
.item .tfjs-container img { filter: invert(100%) }
.item .tfjs-container .tf-table { filter: invert(100%); border-spacing: 10px 5px; margin: 0 -10px; width: calc(100% + 20px); }
.item .tfjs-container { filter: invert(100%) }
.item .tf-table th { background:#171717; padding:0 5px; border-radius:4px; border:0 !important }
.item .tf-table td { padding:0 5px; border:0 !important }
.item .visor-toggle { position:absolute; z-index:1; right:10px; bottom:10px; cursor:pointer; visibility:hidden } /* hidden until tfvis loaded */
/* workaround for width being too large on smaller iPhones */
@media only screen and (max-width: 600px) {
  #tfjs-visor-container { zoom: 0.6 }
  /* hide useless (and buggy) Maximize button */
  .visor button:first-of-type { display: none }
}
.item .visor-toggle { visibility:visible }

<!--/removed-->
#_load #_async #_init #_TensorFlow

#TensorFlow is a machine learning library. See basic #/example, linear regression tutorial, or face detection demo. Default backend is WebGL (GPU). WebGL 2.0 is used if available (may need to be enabled in browser settings). Helper functions in #/util can help with memory management or with switching to the WebAssembly (CPU) backend, which can be faster on smaller problems.

if (!window.tf) {
  await _load('https://cdn.jsdelivr.net/npm/@tensorflow' +
    '/tfjs/dist/tf.min.js')
  await tf.setBackend('webgl') // default backend (can be changed)
  // import #TensorFlow/util _after_ loading tf
  _this.eval(_item('#TensorFlow/util').read('js'))
}
// set log filter for "Could not get context for WebGL version 2"
// so it does not get written into item (still logged in console)
_this.log_options.filter = (entry) =>
  entry.text != 'Could not get context for WebGL version 2'

#_load #async #/util

#TensorFlow/example #_TensorFlow

function main() {
  const J = 1024
  const xJJ = tf.randomNormal([J,J])
  const yJJ = tf.randomNormal([J,J])
  const zJJ = tf.dot(xJJ, yJJ)
}
await tf._run(main) // use default backend (webgl)
await tf._run(main, 'wasm')
tf._run took 75ms using webgl
tf._run took 430ms using wasm

#TensorFlow/vis/example renders a summary of a simple model into both the visor (sidebar) and a container element below. Also opens the visor and renders a toggle link on the lower right using the macro \&lt;&lt;visor_toggle>>.

const backend = 'webgl' // try wasm for smaller models
function main() {
  const model = tf.sequential()
  model.add(tf.layers.dense({inputShape: [1], units: 1}))
  model.add(tf.layers.dense({units: 1}))
  // model.summary(60) // log to console (and _log block)  
  // render into 'summary' container and visor (sidebar)
  render(tfvis.show.modelSummary, 
    ['summary', {name:'model', tab:_this.name}], model)
  tfvis.visor().open()
}
await tf._run(main, backend)

<<tfvis_container('summary')>>
<<tfvis_visor_toggle>>
#_TensorFlow/vis

tf._run took 5ms using webgl

#c3 is a charting library #preloaded as c3 (or window.c3) and extended here with c3._chart, the preferred constructor for c3 charts on MindPage.

function _init() {
  c3._chart = c3_chart; // see below
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
}
function c3_chart(selector, options) {
  let elem = document.querySelector(selector)
  // NOTE: single-item rendering can be useful for debugging
  // if (_that.id != "KqhfMvy3l5OXRzB18Sai") return
  if (!elem) return // element removed
  const rotated = options.axis?.rotated
  const labeled = options.data?.labels
  const barchart = options.data?.type == 'bar'
  let defaults = {
    bindto: selector,
    point: { r: 3 },
    axis: {
      x: {
        show: true,
        tick: { outer: false, multiline: false },
      },
      y: {
        show: !labeled,
        tick: { outer: false, multiline: false },
        padding: {
          bottom: 10,
          top: rotated && labeled ? 70 : labeled ? 40 : 10,
        },
      },
      y2: {
        tick: { outer: false, multiline: false }
      }
    },
    grid: { focus: { show: !barchart } },
    legend: { show: false },
    transition: { duration: 0 }, // disable animations
    // NOTE: onrendered must be used carefully because it is invoked on every render (sometimes apparently multiple times per render) as well as during every tab switch and can cause unnecessary slowdown and potential memory leaks -- it can be useful for debugging chart rendering, e.g. to make sure that no "zombie charts" are getting rendered
    // onrendered: _that.attach(() => {
      // // console.debug('onrendered', elem.id)
      // let elem = document.querySelector(selector)
      // if (!elem) console.error("rendering zombie chart!")
      // if (elem.offsetWidth == 0)
        // console.error("zero-width _chart elem")
    // })
  }
  options = _.merge(defaults, options);
  if (labeled) elem.classList.add("c3-labeled")
  if (rotated) elem.classList.add("c3-rotated")
  if (barchart) elem.classList.add("c3-barchart")
  // ensure element height matches chart height if specified
  if (options.size?.height)
    elem.style.height = options.size.height + 'px';  

  setTimeout(_that.attach(()=>{
    let elem = document.querySelector(selector)
    if (!elem) return; // element was removed
    if (elem.offsetWidth == 0) {
      // NOTE: seems to happen occasionally and without visible issue
      console.warn("zero-width _chart elem")
      _that.invalidate_elem_cache()
      // elem._destroy()
      return;
    }
    // console.debug('c3.generate on ', elem)
    const chart = c3.generate(options)
    // enable various handler functions and required css selectors
    elem.setAttribute("_resize", "")
    elem.setAttribute("_destroy", "")
    elem.setAttribute("_clickable", "")
    //elem._chart = chart
    elem._resize = () => chart.resize() 
    elem._destroy = () => {
      chart.destroy()
      elem.classList.add('c3') // restore .c3 class removed by chart.destroy()
    }      
    elem._clickable = (e) => {
      if (e.target.closest(".c3-legend-item-event")) return true
      // cursor == 'pointer' covers most foreground elements (e.g. bars) but on mobile devices the background .c3-event-rect is also used for (persistent) tooltips since foreground elements can be small
      if (e.target.style.cursor == "pointer") return true
      if (e.target.closest(".c3-event-rect")) return true
      return false // pass clicks through to item
    }
    // start monitoring element width, invalidate cache if 0
    const monitorElemWidth = _that.attach(()=>{
      const elem = document.querySelector(selector)
      if (!elem) return // element removed, stop monitoring
      if (elem.offsetWidth == 0) {
        console.error("zero-width _chart elem")
        _that.invalidate_elem_cache()
        // elem._destroy()
        return // stop monitoring
      }
      setTimeout(monitorElemWidth, 1000)
    })
    monitorElemWidth()
  }), 100);
  // return chart;
}
/* original styles at https://github.com/c3js/c3/blob/master/c3.css */
.c3 { background: #171717; border-radius: 4px; position: relative }
.c3 { break-inside: avoid } /* for multi-column, can be slow */
.c3:not(:first-child) { margin-top: 4px }
.c3:not(:last-child) { margin-bottom: 4px }
.c3 text {
  fill: gray;
  stroke: none;
  font-size: 14px;
  font-family: Avenir Next, Helvetica;
}
.c3 path.domain, .c3 .tick line { stroke: gray }
.c3-tooltip { box-shadow: none }
.c3-tooltip th, .c3-tooltip tr, .c3-tooltip td {
  color: black;
  font-weight: 600;
  background-color: #999;
  border: 1px solid #444;
}
/* reset .item table spacing styles intended for markdown tables */
.item table.c3-tooltip { border-spacing: 0; margin-left: 0 }
.c3-tooltip th { font-weight: 700 }
.c3-grid { opacity: 0.5 }
.c3-line { stroke-width: 3px }
/* .c3-rotated, .c3-barchart are defined in c3_chart above */
.c3:not(.c3-rotated) .c3-text { transform: translate(0, -5px) }
.c3-rotated .c3-axis-x { transform: translate(0, 1px) }
.c3-rotated .c3-texts .c3-text { transform: translate(0, -1px) }
.c3-barchart .c3-axis-x .tick line { display: none }
.c3-barchart .c3-axis-x .domain { display: none }

#_init

#MindBox is the multi-purpose text box at the top of the page.

const MindBox = {
  get: () => MindBox.elem.value,
  set: (text) => { 
    MindBox.elem.value = text
    // also shift selection to the end
    // on Safari, setting the selection can auto-focus so we have to blur
    const wasFocused = document.activeElement.isSameNode(MindBox.elem)
    MindBox.elem.selectionStart = MindBox.elem.value.length
    if (!wasFocused) MindBox.elem.blur()
    // trigger input event for handling of change
    MindBox._input()   
  },
  clear: () => MindBox.set(''),
  toggle: (text) => {
    if (MindBox.get().trim() == text.trim()) MindBox.clear()
    else MindBox.set(text)
  },
  focus: (text) => {
    if (typeof text == 'string') MindBox.set(text)
    MindBox.elem.selectionStart = MindBox.elem.value.length
    MindBox.elem.focus()
    // scroll up to header if necessary
    const header = document.getElementById('header')
    if (document.body.scrollTop > header.offsetTop)
      document.body.scrollTo(0, header.offsetTop)    
  },
  // MindBox.create emulates 'create' button
  // use window._create for creating items w/o history & command parsing
  create: (text, {edit=false,run=false}={}) => {
    if (typeof text == 'string') MindBox.set(text)
    // simulate modified keydown for Enter, then keyup for modifiers
    // modifier keyups are required by MindBox in case modifiers
    // affect behavior of commands or other triggered code
    MindBox._keydown({ 
      code:'Enter',
      metaKey:true,  // first modifier is create shortcut
      ctrlKey:!edit, // second modifier disables editing
      shiftKey:!run  // shift disables running
    })
    MindBox._keyup({code:'Meta'})
    MindBox._keyup({code:'Control'})
    MindBox._keyup({code:'Shift'})
    return window._mindbox_return // can be created item or promise
  },
  // elem getter
  get elem() { 
    return document.getElementById('textarea-mindbox') 
  },
  // event triggers for internal use
  _input: () => { MindBox.elem.dispatchEvent(
    new Event('input')) },
  _keydown: (key) => { MindBox.elem.dispatchEvent(
    new KeyboardEvent('keydown', key)) },
  _keyup: (key) => { MindBox.elem.dispatchEvent(
    new KeyboardEvent('keyup', key)) },
}
// make global for easy access from HTML and non-dependents
// (only dependents will auto-update with latest code)
function _init() { window.MindBox = MindBox; }

#webppl/run is a promise-based wrapper for webppl.run.

function webppl_run(code, options) {
  return webppl.__run = Promise.allSettled([webppl.__run]).then(()=>
    new Promise((resolve, reject) => {
      const start = Date.now()
      try { 
        webppl.run(code, (s, x) => {
          if (_.keys(s).length > 0) // log webppl state
            console.log("webppl state:", JSON.stringify(s));
          console.log(`webppl took ${Date.now()-start}ms`)
          resolve(x);
        }, _.merge({ errorHandlers: [reject] }, options))
      } catch (e) { reject(e) }
    })
  )
}

#features/_listen tag enables code to be evaluated on certain events:

function _on_search(text) { alert("You searched for: " + text) }
function _on_create(text) { alert("You created: " + text) }

#features/_async tag enables async evaluation.

#Features/_autorun tag enables item to be auto-run on changes to #dependencies.

#Features/_spell tag manually enables spell-checking on item. #//_nospell can be used to manually disable spell-checking. Without these tags, spell-checking is enabled automatically for items that do not contain any code blocks (delimited by ```).

#Features/_nospell tag manually disables spell-checking on item. Also see #//_spell.

#macros are #code that generate content.

const px = (s) => typeof s == 'number' ? s+'px' : s
const img = (u, w='auto', h='auto', style='') => `&lt;img src="${u}" style="width:${px(w)};height:${px(h)};${style}">`
const code = (text) => "`" + text + "`"
const block = (type, content) => '```'+type+'\n' + content + '\n```'
const html = (content) => block('_html', content)
const json = (obj) => block('json',JSON.stringify(obj, null, 2)
  ?.replace(/"([^"]+)":/g,'$1:')/*?.replace(/,([^\s\d])/g,", $1")*/)
// NOTE: using onmousedown + cancelled onclick maintains keyboard focus and is generally more robust, especially on mobile devices w/ virtual keyboards
const jslink = (js, text=js, classes='', style='', title=js) => 
  `&lt;a href="#" onmousedown="` + _.escape(js) + 
    `;event.preventDefault();event.stopPropagation()" ` +
    `onclick="event.preventDefault();event.stopPropagation()" ` +
    `class="${classes}" style="${style}" title="${_.escape(title)}">${text}&lt;/a>`
const evallink = (item, js, text=js, classes='', style='', title=js) =>
  jslink(`_item('${item.id}')` +
    '.eval(`' + js.replace(/([`\\$])/g,"\\$1") + '`)', text, classes, style, title)
const cmdlink = (cmd, text=cmd, options='', classes='', style='', title=cmd) =>
  jslink(`MindBox.create(\`${cmd}\`,{${options}})`, text, classes, style, title)
const spacer = (height=10, width=10) => 
  `&lt;div style="height:${height}px; width:${width}px">&lt;/div>`
const time_short = (ms) => (
    ms &lt; 60*1000 ? "&lt;1m" :
    ms &lt; 60*60*1000 ? Math.floor(ms/(60*1000)) + "m" :
    ms &lt; 24*60*60*1000 ? Math.floor(ms/(60*60*1000)) + "h" :
    Math.floor(ms/(24*60*60*1000)) + "d"
  )
// register most common macros globally
// importing via #_macros is optional for auto-updating
// also allows _this to refer to invoking item (vs #macros) as expected
// we avoid use of _this in global macros to avoid this ambiguity
function _init() {
  window.img = img
  window.code = code
  window.block = block
  window.html = html
  window.json = json
  window.jslink = jslink
  window.evallink = evallink
  window.cmdlink = cmdlink
  window.spacer = spacer
}

#_init

#MindPage/core/_Item object holds a reference to a MindPage item and provides access to a wide range of properties and functions:

#commands/restore command /restore [name|version] restores an existing or deleted item from a past version.

async function run(arg) {
  if (_user.uid == 'anonymous') {
    alert('/restore command is not useful for anonymous accounts')
    return
  }
  const limit_per_item = 50
  const limit_for_deletions = 100 // need extra to skip non-deleted
  if (!arg) { // list recent deletions
    // const res = await window.firebase.firestore()
      // .where("user", "==", _user.uid) // security requirement
      // .orderBy("time", "desc")
      // .limit(limit_for_deletions).get()
    const { getDocs, query, collection, getFirestore, where, orderBy, limit } =
      firebase.firestore
    const res = await getDocs(query(
      collection(getFirestore(firebase), "history"), 
        where("user", "==", _user.uid), // security requirement
        orderBy("time", "desc"),
        limit(limit_for_deletions)
    ))
    let versions = []
    let deleted = new Set() // set of deleted item ids
    res.forEach((doc)=>{
      const version = doc.data()
      if (deleted.has(version.item)) return // skip older version
      if (_exists(version.item)) return // skip non-deleted
      deleted.add(version.item) // found deleted item
      if (!version.text && !version.cipher)
        console.error('unexpected history item', version)
      let bytes = (version.text || version.cipher || '').length + ' bytes'
      if (version.text === undefined) bytes += ' (encrypted)'
      const date = new Date(version.time)
      versions.push(`id:${version.item}  ${date.toLocaleTimeString()}  ${date.toLocaleDateString()}  ${bytes}`)
    })
    if (versions.length == 0) {
      alert(`no recent deletions found`)
    } else {
      const count = versions.length + 
        (versions.length == limit_per_item ? '+':'')
      return { // create item for easy examination of listing
        text:[`${count} deleted items found:`,
              '```', versions, '```'].flat().join('\n'),
      }
    }
        
  } else if (!arg.startsWith('version:') &&
    (arg.startsWith('#') || arg.startsWith('id:') || _item(arg))) { 
    // treat arg as existing item #label|id or deleted item id
    let id = arg.replace(/^id:/, '') // drop id: prefix
    if (arg.startsWith('#')) { // look up id from #name
      if (!_item(arg)) { alert(`item ${arg} not found`); return }
      id = _item(arg).id
    }
    // const res = await window.firebase.firestore()
      // .collection("history")
      // .where("user", "==", _user.uid) // security requirement
      // .where("item", "==", id)
      // .orderBy("time", "desc")
      // .limit(limit_per_item).get()
    const { getDocs, query, collection, getFirestore, where, orderBy, limit } =
      firebase.firestore
    const res = await getDocs(query(
      collection(getFirestore(firebase), "history"), 
        where("user", "==", _user.uid), // security requirement
        where("item", "==", id),
        orderBy("time", "desc"),
        limit(limit_per_item)
    ))
      
    let versions = []
    res.forEach((doc)=>{
      const version = doc.data()
      let bytes = (version.text || version.cipher || '').length + ' bytes'
      if (version.text === undefined) bytes += ' (encrypted)'
      const date = new Date(version.time)
      versions.push(`version:${doc.id}  ${date.toLocaleTimeString()}  ${date.toLocaleDateString()}  ${bytes}`)
    })
    if (versions.length == 0) {
      alert(`no versions found for item '${arg}'`)
    } else {
      const count = versions.length + 
        (versions.length == limit_per_item ? '+':'')
      return { // create item for easy examination of listing
        text:[`${count} versions found for item ${arg}:`,
              '```', versions, '```'].flat().join('\n'),
      }
    }

  } else { // treat arg as version
    const id = arg.replace(/^version:/, '') // drop version: prefix
    let docref
    try { // missing doc can throw permission error
      // doc = await window.firebase.firestore()
      //  .collection("history").doc(id).get()
      const { getDoc, doc, getFirestore } = firebase.firestore
      docref = await getDoc(doc(getFirestore(firebase), 'history', id))      
    } catch (e) { console.error(e) }
    if (!docref.exists()) { alert(`could not find version '${arg}'`); return }
    const item = await _decrypt_item(docref.data())
    // NOTE: we prefix #restored label to avoid conflicts/errors
    return { text:'#restored ' + item.text }
  }
}

#_async

#commands are #code run via #MindBox. Examples: #/hello #/hello_again #/js #/alert #/copy #/export #/restore #/agenda #/invert #/weight.

#commands/copy command /copy name copies an item. Item name can be specified as either #label or id. For example, this item can be specified as either &lt;&lt;_this.label>> or &lt;&lt;_this.id>>.

const run = (name) => ({text:_item(name).read()})

#typescript enables TypeScript. See #/example.

if (!window.ts) {
  const url_base = 'https://unpkg.com/typescript@4.2.3/lib'
  if (!window.ts_libs) window.ts_libs = {}
  const ts_lib = (n) => !!window.ts_libs[n] || fetch(url_base + `/lib.${n}.d.ts`).then(r=>r.text()).then(t=>window.ts_libs[n]=t)
  await _load(url_base + '/typescriptServices.js',
    ts_lib('es5'), ts_lib('es6'), ts_lib('dom'))
}
// enable 'run' for (typescript|ts)_input blocks
function _run() {
  const ts = '(()=>{ ' + _this.read_input('(typescript|ts)') + '\n})()'
  const start = Date.now()
  const js = transpile(ts).trim().split('\n').slice(1,-1).join('\n')
  console.debug('typescript transpiler took', Date.now()-start, 'ms')
  return _this.eval(js, {async:true}).catch((e)=>{})
}
// basic ts->js transpiler that logs all errors to console
function transpile(input) {
  // options mimic ts.transpileModule
  // https://github.com/microsoft/TypeScript/ 
  //   blob/master/src/services/transpile.ts
  const options = ts.getDefaultCompilerOptions()
  options.suppressOutputPathCheck = true
  options.allowNonTsExtensions = true
  options.lib = Object.keys(ts_libs)
  if (!window.ts_files) window.ts_files = Object.fromEntries(
    Object.entries(ts_libs).map(([n, t])=>[n,ts.createSourceFile(n,t)]))  
  const fname = _this.name
  const file = ts.createSourceFile(fname, input, options.target)
  window.ts_files[fname] = file  
  const newLine = ts.getNewLineCharacter(options)
  if (!window.ts_output) window.ts_output = {}
  // NOTE: using fewer arguments for compiler host (vs transpileModule)
  if (!window.ts_compiler) window.ts_compiler = {
    getSourceFile: (name) => ts_files[name],
    writeFile: (name, text) => { window.ts_output[name] = text },
    getDefaultLibFileName: () => '',
    useCaseSensitiveFileNames: () => false,
    getCanonicalFileName: fileName => fileName,
    getCurrentDirectory: () => '',
    getNewLine: () => newLine
  }
  const program = ts.createProgram([fname], options, ts_compiler)
  // diagnostics output mimics typescript-simple
  // https://github.com/teppeis/typescript-simple/blob/master/index.ts
  const diagnostics = program.getSyntacticDiagnostics(file)
    .concat(program.getSemanticDiagnostics(file))
    .concat(program.getOptionsDiagnostics())
  if (diagnostics.length > 0) logDiagnostics(diagnostics)
  program.emit()
  return ts_output[fname+'.js'] // .js appended by compiler
}
function logDiagnostics(diagnostics) {
  return diagnostics.forEach((d) => {
    if (d.file && typeof d.start === 'number') console.error(d.messageText + ` (line:${d.file.getLineAndCharacterOfPosition(d.start).line})`)
    else console.error(d.messageText)
  })
}

#_load #_async

#python enables Python via Brython. See #/example.

if (!window.__BRYTHON__) {
  const _brython = 'https://cdn.jsdelivr.net/npm/brython@3.9.1'
  await _load(_brython + '/brython.min.js',
              _brython + '/brython_stdlib.js')
}
// enable 'run' for python_input blocks
function _run() {
  try {
    __BRYTHON__.debug = 0
    const js = __BRYTHON__.python_to_js(_this.read_input('python'))
    return _this.eval(js, {async:true})
  } catch (e) {
    console.error(`#python error: ${e.args[0]} (line:${e.$line_info})`)
  }
}

#_load #_async

#macros/animation macro generates animation on a canvas. Tap canvas to play/pause. Can be customized using an initializer (init) function that returns the context for drawing. The initializer can be async or return a promise, e.g. for loading external libraries. See #webgl for a basic initializer for GPU acceleration.

const animation_options = (options={}) => _.merge({
  zoom: 1/devicePixelRatio, // zoom factor (visible px / canvas px)
  target_fps: 60,  // target frames/sec (lower to reduce load)
  autoplay: false, // start playing immediately or require click
  start: 0,        // starting time (secs, determines initial frame)
  period: 3,       // period (secs) for repeats (0 means infinite)
  style: '',       // canvas element style (anything except zoom)
  div_style: '',   // style for animation container div
  reset_style: '', // style for reset button
  play_style: '',  // style for play button
  time_style: '',  // style for time text element
  fps_style: '',   // style for fps text element
  status_style: '',// style for status text element
  time_format: '.1f', // time (secs) text format
  time_prefix: '',    // prefix for time text
  time_suffix: 's',   // suffix for time text
  fps_suffix: ' fps', // suffix for fps text
  show_reset: options.period==0, // show reset button?
  show_fps: true,                // display fps?
  // context initializer function (can be async return promise)
  init: (canvas) => canvas.getContext('2d')
}, options)

function animation(draw = (ctx, t, w, h) => {}, options = {}) {  
  options = animation_options(options)
  if (!window._animations) window._animations = {}
  window._animations['$cid'] = {init:options.init, draw, cid:'$cid', item:_this}
  return html(_item('$id').read('html') // see html section
    .replaceAll('%cid', '$cid') // use $cid of macro instead of script
    .replaceAll('%div_style', options.div_style)
    .replaceAll('%style', options.style)
    .replaceAll('%div_style', options.div_style)    
    .replaceAll('%fps_style', options.fps_style)
    .replaceAll('%time_style', options.time_style)
    .replaceAll('%status_style', options.status_style)
    .replaceAll('%reset_style', options.reset_style)    
    .replaceAll('%options', JSON.stringify(options)))
}
&lt;!-- *_removed block is not included in rendered item -->
&lt;!-- %cid is a unique content identifier based on macro usage -->
&lt;div id="animation-%cid" class="animation" style="%div_style">
&lt;div class="fps" style="%fps_style">&lt;/div>
&lt;div class="time" style="%time_style">&lt;/div>
&lt;div class="status" style="%status_style">&lt;/div>
&lt;div class="play" style="%play_style">&lt;/div>
&lt;div class="reset" style="%reset_style">reset&lt;/div>
&lt;canvas style="%style">&lt;/canvas>
&lt;script>
const options = JSON.parse(`%options`)
const div = document.querySelector('#animation-%cid')
const canvas = div.querySelector('canvas')
const fps_elem = div.querySelector('.fps')
const time_elem = div.querySelector('.time')
const status_elem = div.querySelector('.status')
const play_elem = div.querySelector('.play')
const reset_elem = div.querySelector('.reset')
const time_formatter = d3.format(options.time_format)
canvas.width = canvas.offsetWidth / options.zoom
canvas.height = canvas.offsetHeight / options.zoom
canvas.style.zoom = options.zoom
let pause = !options.autoplay
if (options.autoplay) play_elem.style.display = 'none'
// show/hide reset button based on option
// show even if t=start because reset may affect first frame
reset_elem.style.display = options.show_reset ? 'block' : 'none'
let stopped = false
let stopTime = 0
// NOTE: startTime is used to calculate animation time (t) and can be adjusted arbitrarily by the animation drawing function to modify t
let startTime = 0 // set inside update() so first frame is t=start
div.style.cursor = 'pointer'
div.onclick = (e) => {
  e.stopPropagation()
  if (!pause) pause = true
  else if (stopped) {
    pause = stopped = false
    play_elem.style.display = 'none'
    fps_elem.style.display = options.show_fps ? 'block' : 'none'
    if (startTime == 0) console.warn('unpaused with startTime==0')
    else startTime += (Date.now() - stopTime) // skip stopped time
    fps = 0
    frameCount = 0
    frameCountStartTime = Date.now()
    lastFrameRequestTime = Date.now()
    update() 
  }
  _this.touch() // soft touch item
}
reset_elem.onclick = (e) => {
  e.stopPropagation()
  startTime = stopTime = 0
  if (stopped) update() // update frame and set start/stopTime
  _this.touch() // soft touch item
}
const _draw = _animations['%cid'].draw
const store = _animations['%cid'].store = {}
_animations['%cid'].elem = div
let fps = 0
let frameCount = 0
let frameCountStartTime = 0
let lastFrameRequestTime = 0
let update
_this.resolve(_animations['%cid'].init(canvas)).then((ctx) => {
  div.classList.add('initialized')
  frameCountStartTime = Date.now()
  lastFrameRequestTime = Date.now()
  update = () => {
    if (canvas._update != update) return // cancelled update loop
    // pause animation if canvas is not on non-hidden item
    if (!_this.elem?.contains(canvas) || _this.elem?.matches('.hidden'))
      pause = true
    // calculate time and draw frame
    if (startTime == 0) startTime = Date.now() - options.start * 1000
    let t = (Date.now() - startTime) * 0.001
    if (options.period > 0) t = t % options.period
    const obj = _draw(ctx, t, canvas.width, canvas.height, {pause, store, item:_this, cid:'%cid'})
    // draw function can pause, reset, adjust t, display status
    // (not documented yet, subject to change)
    if (typeof obj == 'object') {
      if (obj.reset) {
        startTime = stopTime = 0 // ensures next frame is t=start
        update() // update frame and set start/stopTime
        return
      }
      if (obj.pause) pause = true
      if (typeof obj.t == 'number') {
        const t_now = (Date.now() - startTime) * 0.001
        startTime += (t_now - obj.t) * 1000 // ensures t_now == obj.t
      }
      if (typeof obj.status == 'string') {
        status_elem.textContent = obj.status
        status_elem.style.display = obj.status ? 'block' : 'none'
      }
    }
    // format and display time
    time_elem.textContent = options.time_prefix + time_formatter(t) + options.time_suffix
    // calculate and display fps
    if (options.show_fps) {
      frameCount++
      const now = Date.now()
      if (now - frameCountStartTime > 1000) {
        fps = 1000 * frameCount / (now - frameCountStartTime)
        frameCountStartTime = now
        frameCount = 0
      }
      fps_elem.textContent = (fps == 0 ? '⋯' : fps.toFixed(0)) + options.fps_suffix
    }
    if (pause) { // paused, stop updating
      stopped = true
      stopTime = Date.now()
      play_elem.style.display = 'block'
      fps_elem.style.display = 'none'
      return
    } else if (stopped) console.warn('unpaused while stopped')
    
    // dispatch next frame, controlling for frame rate
    const nextFrame = () => {
      lastFrameRequestTime = Date.now()
      requestAnimationFrame(update)
    }
    const fps_delay = Math.max(0, 
      1000/options.target_fps - (Date.now() - lastFrameRequestTime))
    if (fps_delay == 0) nextFrame()
    else setTimeout(nextFrame, fps_delay)
  }
  canvas._update = update // cancels any other update tasks
  update() // start updating
}).catch(console.error)
&lt;/script>&lt;/div>
function _init() {
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
}
.animation { background:#171717; width:fit-content; position: relative; display: inline-block; user-select: none; -webkit-user-select: none }
.animation:not(.initialized) > * { visibility: hidden; }
.animation > .fps { position: absolute; top:0; right:0; padding:5px; background:rgba(0,0,0,.75); border-bottom-left-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .time { position: absolute; top:0; left:0; padding:5px; background:rgba(0,0,0,.75); border-bottom-right-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .reset { position: absolute; bottom:0; left:0; padding:5px; background:rgba(0,0,0,.75); border-top-right-radius: 4px; font-size: 80%; line-height: 100%; color:gray }
.animation > .status { position: absolute; bottom:0; right:0; padding:5px; background:rgba(0,0,0,.75); border-top-left-radius: 4px; font-size: 80%; line-height: 100%; color:gray; display: none }
.animation > .play { position: absolute; top:50%; left:50%; margin-left:-20px; margin-top:-20px; border-left: 40px solid rgba(255,255,255,.5); border-top: 20px solid transparent; border-bottom: 20px solid transparent }
.animation > canvas { border-radius: 4px; vertical-align: middle }
.animation:not(:first-child) { margin-top: 4px; }
.animation:not(:last-child) { margin-bottom: 4px; }

#_init

#webppl is a probabilistic programming language. Here we load the library and extend it with #/run, #/summarize, and #/util. See example HMM.

if (!window.webppl) {
  // cdn url from: https://probmods.github.io/webppl-editor/ 
  await _load('https://s3-us-west-2.amazonaws.com' +
    '/cdn.webppl.org/webppl-v0.9.9.js')
}
// enable 'run' for webppl_input blocks
const _run = () => webppl._run(_this.read_input('webppl'))
// javascript functions attached to webppl
webppl._run       = webppl_run       // for js
webppl._summarize = webppl_summarize // for webppl
webppl._samples   = webppl_samples   // for webppl
// webppl functions from #webppl/util: _enumerate

#_load #_async #_webppl/run #_webppl/util #_webppl/summarize

#commands/weight command /weight weight records today's weight in item #weight.

function run(weight) {
  if (!(weight > 0)) { // also rejects empty/non-numeric strings
    alert(`invalid weight '${weight}'`)
    return '/weight ' // clear for easy re-entry
  }
  const data_item = '#weight' // item containing _data
  const today = new Date().toLocaleDateString()
  let data = _item(data_item).read('_data')
  if (data.startsWith(today)) data = data.replace(/^.+?\n/,"")
  _item(data_item).write(`${today}  ${weight}\n` + data, '_data')
}

#webgl defines a basic webgl canvas context initializer webgl_init that only requires a "pixel" shader that assigns a color to each pixel. An example shader and basic animation drawing function (for #macros/animation) is also defined and demonstrated below.
<<animation(webgl_draw,{init:webgl_init(_this.read('glsl_example'))})>>

precision mediump float; 
uniform float t, w, h;
void main(void) {
  float x = gl_FragCoord.x / w;
  float y = 1.0 - gl_FragCoord.y / h;
  float r = (x + y) * 0.5;
  float a = pow(min(1.0, max(0.0, 1.0 - abs(t/3.0 - r))), 10.0);
  gl_FragColor = vec4(a); // premultiplied alpha reduces fringing
}
// basic reusable drawing function
function webgl_draw(gl, t, w, h) {
  gl.viewport(0, 0, w, h)
  // write t, w, h for shader (see webgl_init below for setup)
  gl.uniform1f(t_loc, t)
  gl.uniform1f(w_loc, w)
  gl.uniform1f(h_loc, h)
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4) // invokes shader
}
let t_loc, w_loc, h_loc
function webgl_init(shader) { return (canvas) => {
  const gl = canvas.getContext('webgl')
  // enable alpha channel, premultiplied to reduce fringing
  // http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
  gl.enable(gl.BLEND)
  //gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)  
  gl.blendFunc(gl.ONE /*premultiplied*/, gl.ONE_MINUS_SRC_ALPHA)
  // compile vertex shader
  const vs = gl.createShader(gl.VERTEX_SHADER)
  gl.shaderSource(vs, 'attribute vec2 c; void main(void) { gl_Position = vec4(c, 0.0, 1.0); }')
  gl.compileShader(vs)
  // compile fragment shader
  const fs = gl.createShader(gl.FRAGMENT_SHADER)
  gl.shaderSource(fs, shader)
  gl.compileShader(fs)
  // link program
  const prog = gl.createProgram()
  gl.attachShader(prog, vs)
  gl.attachShader(prog, fs)
  gl.linkProgram(prog)
  gl.useProgram(prog)
  // set up vertex shader coordinate array buffer
  const vb = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, vb)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(
    [-1, 1, -1, -1, 1, -1, 1, 1]), gl.STATIC_DRAW)
  const c_loc = gl.getAttribLocation(prog, "c")
  gl.vertexAttribPointer(c_loc, 2, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(c_loc)  
  // set up fragment shader inputs
  t_loc = gl.getUniformLocation(prog, "t")
  w_loc = gl.getUniformLocation(prog, "w")
  h_loc = gl.getUniformLocation(prog, "h")
  return gl
} }

#_macros/animation

#features/_pin/dot tag designates dotted items:

#bounce macro renders text that "bounces" text as in the ◄ tap! prompt in the welcome item. The bounce starts at welcome ends after a prescribed search or a timeout period.

const bounce = (text, search="#MindPage", ms=60000) => `&lt;span class="bounce" _time="${ms}" _search="${search}" _cached>${text}&lt;/span>`
function _on_welcome() {
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
  document.querySelectorAll('.bounce').forEach((elem)=>{
    setTimeout(()=>{
      elem.classList.add('stop')
    }, parseInt(elem.getAttribute('_time')))
  })
}
function _on_search(text) {
  document.querySelectorAll('.bounce').forEach((elem)=>{
    if (text.toLowerCase().startsWith(
      elem.getAttribute('_search').toLowerCase())) {
      elem.style.opacity = 0
      elem.classList.remove('bounce')
    }
  })
}
function _on_create(text) {}
@keyframes bounce {
  0% { transform: translateX(0) scale(1); color: #aaa; }
  100% { transform: translateX(25px) scale(1.3); color: #fff; }
}
.bounce {
  display: inline-block;
  animation-duration: .35s;
  animation-name: bounce;
  animation-iteration-count: 100;
  animation-direction: alternate;
  animation-timing-function: ease-out;
}
.bounce.stop {
  animation-name: none;
}

#_welcome #_listen

#charts can be generated using #macros such as #macros/bar_chart #_macros/bar_chart:
<<bar_chart([['y1',4,2,5,10],['y2',2,3,6,12]],'height:200px')>>

Alternatively, use #c3 directly from an _html block:

&lt;div id="chart-$id" class="c3" style="height:160px">&lt;script>
c3._chart('#chart-$id', {
  data: { columns: [['y1',4,2,6,10],['y2',2,3,7,12]] },
  axis: {
    x: { padding: .1 },
    y: {
      min:0, tick: { values:[0, 5,10] },
      padding: { bottom:0, top:10 }
    }
  }
});
&lt;/script>&lt;/div>

#load function loads external libraries. Examples: #mathjs #jStat #webppl #TensorFlow #gapi, ...

await _load(
  window.lib1 || "url1", // load only if !window.lib1
  window.lib2 || "url2", // load only if !window.lib2
  // custom_init(),
)
// ... post-load init

// Alternative Usage (assuming lib1 primary)
if (!window.lib1) { // skip load+init if lib1 loaded
  await _load("url1", "url2", /* custom_init() */)
  // ... post-load init
}
// loads given url string(s)
// flattens out arrays, skips non-strings
// resolves any Promise arguments simultaneously with url loads
//   (allows simultaneous custom init besides url loading)
window._load = (...urls) => 
  Promise.all(_.flattenDeep([...urls])
    .filter((u)=>(typeof u === 'string')).map(
      (src) => new Promise((resolve, reject) => {
        const start = Date.now()
        console.debug(`loading url '${src}' ...`)
        let script = document.createElement('script')
        script.src = src
        script.onload = () => {
          console.debug(`loaded url '${src}' in ${Date.now()-start}ms`)
          resolve()
        }
        script.onerror = reject
        document.head.appendChild(script)
      })).concat(_.flattenDeep([...urls])
        .filter((u)=>u instanceof Promise)))

#mathjs/example #_mathjs

return math.stirlingS2(10,5)
42525

#jStat/example #_jStat

return jStat.normal(0,1).sample()
-1.138345570479304

#MindPage/core consists of #/properties, #/functions, #/commands, and #preloaded libraries that you can use to build your own #macros and #commands. You can also #load any external libraries that you need. Full source code is available at https://github.com/olcan/mind.page. #_context

#code can be inline or inside blocks:

const say_hello = () =>  "hello world!"

#features/_welcome tag enables "welcome" code to be evaluated after the initial page has been rendered and any welcome modal has been dismissed.

function _welcome() { alert("Welcome to MindPage!") }

#graphviz is a graph visualization library #preloaded via its js/wasm port and extended here with graphviz._graph, the preferred constructor for graphviz graphs on MindPage.

function _init() {
  window.graphviz = window["@hpcc-js/wasm"].graphviz
  graphviz._graph = graphviz_graph; // see below
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
}
.dot {
  padding: 5px 0;
  width: fit-content; /* left-aligns graph if width:auto */
  display: inline-block;
  vertical-align: top;
  cursor: default;
}
.dot > svg {
  display: block;
  height: 100%;
  width: auto;
  max-width: 100%;
  margin: auto;
}
.dot .graph text { stroke: none !important }
.dot .node, .dot .edge { cursor: pointer }
// best way to define defaults for dot graphs is to insert dot attributes, which are turned into SVG attributes by Graphviz (hpcc-js/wasm), which then take lowest priority and can be easily modified either in the dot code or using CSS
// for layouts, see https://www.graphviz.org/documentation/
function graphviz_graph(selector, dot, layout = "dot") {
  const nodedefs = `color="#999999", fontcolor="#999999", fontname="Avenir Next, Helvetica", fontsize=20, shape=circle, fixedsize=true`;
  const edgedefs = `color="#999999", fontcolor="#999999", fontname="Avenir Next, Helvetica",penwidth=1`;
  const graphdefs = `bgcolor=invis; color="#666666"; fontcolor="#666666"; fontname="Avenir Next, Helvetica"; fontsize=20; nodesep=.2; ranksep=.3; node[${nodedefs}]; edge[${edgedefs}]`;
  const subgraphdefs = `labeljust="r"; labelloc="b"; edge[minlen=2]`;
  dot = dot.replace(/(subgraph.*?{)/g, `$1\n${subgraphdefs};\n`);
  dot = dot.replace(/(graph.*?{)/g, `$1\n${graphdefs};\n`);
  graphviz.layout(dot, "svg", layout).then(_that.attach(svg => {
    let elem = document.querySelector(selector);
    if (!elem) {
      // console.error("missing _graph elem", selector)
      _that.invalidate_elem_cache()
      return 
    }
    elem.innerHTML = svg;
    // declare nodes/edges clickable and block default handlers
    elem.setAttribute("_clickable", "")
    const clickables = '.node,.edge,.cluster' 
    elem._clickable = (e) => {
      e.stopPropagation()
      return e.target.closest(clickables)
    }
    // NOTE: additional handlers can prevent errors e.g. for long clicks
    elem.onmousedown = (e) => {
      if (e.target.closest(clickables)) e.stopPropagation() }
    elem.onmouseup = (e) => {
      if (e.target.closest(clickables)) e.stopPropagation() }
    elem.onclick = _that.attach((e) => {
      if (!e.target.closest(clickables)) return
      e.stopPropagation()
      _that.touch()
      let text, title
      if (e.target.closest('a')) { // extract xlink:title attribute
        text = e.target.closest('a')
          .querySelector('text')?.textContent
        title = e.target.closest('a').getAttribute('xlink:title')
      } else { // extract &lt;title> tag
        text = e.target.closest(clickables)
          .querySelector('text')?.textContent
        title = e.target.closest(clickables)
          .querySelector('title')?.textContent
      }
      if (text && title && text != title) _modal(text+': '+title)
      else if (text || title) _modal(text || title)
    })
    // dispatch to check rendering if element
    setTimeout(_that.attach(()=>{
      if (!_that.elem?.contains(elem)) {
        // console.error("detached _graph elem", selector)
        _that.invalidate_elem_cache()
        return 
      }
      // _dot_rendered renders $math$ (via #mathjax)
      // and implements class=stack for clusters
      _dot_rendered(_that, elem)
    }))
  }))
}

#_init

#graphs #_macros/graph can be generated using #macros/graph:
<<graph('graph{a--b}')>>
<<graph('digraph{rankdir=LR;a->c;b->c}')>>
<<graph('digraph{a->c;b->c;c->c;b->d}')>>

Alternatively, use #graphviz directly from an _html block:

&lt;div id="dot-$cid" class="dot" style="height:120px">&lt;script>
graphviz._graph('#dot-$cid', _this.read('dot')) // see dot block below
&lt;/script>&lt;/div>
digraph {
  rankdir=LR; 
  x1->x2->x3->x4;
  x1->x3; x2->x4;
  x1[label="$x_1$"];
  x2[label="$x_2$"];
  x3[label="$x_3$"];
  x4[label="$x_4$"];
}

#private means that your items are readable only by you, on your devices. This is achieved using encryption based on a personal secret that is never sent or stored anywhere, as can be verified from open source code. This is much more private than using authentication alone, which still leaves your content readable to service providers, who often also use closed source code that can not be verified.

#Items are small units of content ordered dynamically by recency and relevance. Create items using create button or #⇧⏎ shortcut. Organize by #/naming or tagging. #_context

intro #todo #log #_menu #_pin/dot/2

#items/ordering is determined primarily by recency as you edit or interact with items, and relevance as you type into #MindBox.

#MindPage/core/functions/_modal opens a modal window for basic confirmation and input prompts. Programmable click handlers can be used to trigger certain actions that can otherwise be disallowed in some browsers (e.g. iOS Safari), such as:

Welcome <<_user.name>> to your MindPage!

Getting Started

Edit: just tap on an item. Try this one!
Create: tap button above or use ⇧⏎ shortcut.
Learn more: review intro or cheat sheet.
Sign out: tap on your image <<_user.image>> above.

#items/naming is based on starting #name tags.

#⇧⏎ is the universal shortcut to create or save items in MindPage. You can also use the corresponding buttons indicated in <font color=#6f6>green</font>.

#MindPage/core/functions/_modal/example shows a modal that triggers MindBox.focus() from its confirmation click handler, as required by some browsers (e.g. iOS Safari) that otherwise block keyboard focus actions.

_modal({
  content: "This is an example modal dialog.", 
  confirm: "Focus on MindBox",
  cancel: "Cancel",
  background: "cancel",
  onConfirm: () => MindBox.focus()
})

#features/_init tag enables initialization code.

function _init() { console.log("Welcome to MindPage!") }

#text can be formatted with #/emphasis, #/lists, #/headings, #/blockquotes, and most other Markdown syntax.

#commands/invert command <<cmdlink('/invert')>> inverts colors persistently.

const run = () => {
  document.documentElement.classList.toggle("invert",
    _this.local_store.invert = !_this.local_store.invert)
}
function _init() {
  document.head.insertAdjacentHTML("beforeend", "&lt;style>" +
    _this.read('css') + "&lt;/style>") // see below
  document.documentElement.classList.toggle("invert",
    _this.local_store.invert == true)
}
html.invert { filter: invert(100%) }
html.invert img { filter: invert(100%) }
html.invert textarea { caret-color: #0ff; } /* keep red caret */

#_init

#commands/alert command <<cmdlink('/alert text')>> displays an alert box with given text.

function run(text) { alert(text); return '/alert ' }

#commands/hello_again command <<cmdlink('/hello_again')>> creates an item named #hello and then writes output and log messages into it.

const run = () => ({
  text:'#hello world!',
  edit:false, // no editing
  init:(item) => {
    console.log("initing item ", item.name);
    item.invoke(()=>{ console.log("hello world!") })
    item.write('hello world!') // write _output
    item.write_log() // write _log
  }
})

#commands/hello command <<cmdlink('/hello')>> creates an item to say hello.

const run = () => ({text:`hello world!`})

Upcoming Events

2/13/2021 9:00:00 PM Weekly Review
2/14/2021 11:30:00 PM Copleri hazirla
2/17/2021 1:30:00 PM Temizlik
2/20/2021 9:00:00 PM Weekly Review
2/21/2021 11:30:00 PM Copleri hazirla

#commands/debug creates a debug version of an item to help debug a recent evaluation performed on that item.

function run(args) {
  // parse args as: /debug name [trigger = run]
  let name, trigger;
  ({name, trigger} = _.merge({ trigger: "run" },
    args.match(/^(?&lt;name>\S+)\s*(?&lt;trigger>\S+)?\s*$/)?.groups))
  if (!name) { 
    alert("usage: /debug name [trigger = run]");
    return "/debug ";
  }
  const item = _item(name)
  const text = item.debug_store[trigger];
  if (!text) {
    const triggers = _.keys(item.debug_store)
    alert(`/debug: eval trigger '${trigger}' not found for item '${name}'; available triggers are: ` + triggers.join(", "))
    return `/debug ${args}`;
  }
  return {text:"#_debug " + text, run:false}
}

#MindPage/core/properties are:

#MindPage/core/commands are:

#text/blockquotes

this is
a blockquote

that can be nested
inside another blockquote

#jStat #_load #_async is a statistics library. See #jStat/example.

if (!window.jStat) {
  await _load('https://cdn.jsdelivr.net/npm' +
    '/jstat@1.9.5/dist/jstat.min.js')
}

#Dependencies are created using hidden tags.

#code/run code inside js_input blocks using blue run button or ⌥⏎ while editing. #_code

console.log(say_hello()) // goes into _log block
say_hello() // goes into _output block
hello world!
hello world!

#features/_debug tag disables all code wrapping (e.g. for #features/_async) to enable debugging commands such as #commands/debug.

#features/_context tag enables item to be brought up as context for other items that are tagged in it. This feature is similar to using nested names but allows arbitrary names as long as they are unique.

#features/_menu tag enables special styling where tags and links expand to make them as large as possible for easy tapping.

#features/_pin tag pins item at the top.

#items/tagging is based on #hashtags.

#features/log label designates log items:

Yet another example #todo item.

#todo This is another example todo item.

#todo tag is commonly used as a label for todo items. This is an example.

#math can be inline: $e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$, between lines:
$$e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$$,
in blockquotes:

$$e^x=\sum_{k=a}^\infty \frac{x^k}{k!}$$,
or inside _math blocks:

e^x=\sum_{k=a}^\infty \frac{x^k}{k!}

#Key_symbols

Symbol Key
Shift
Return (Enter)
Command (Windows)
Control
Option (Alt)
Tab
Backspace (Delete)
Escape
Up Arrow
Down Arrow

#webppl/util utility functions for #webppl.

var _enumerate = function(func) { Infer({method:'enumerate'}, func) }
const webppl_samples = (dist) => dist.samples.map((s)=>s.value)

#MindPage/core/properties/_that is the currently evaluating item at the bottom of the evaluation stack, equivalent to _item(_stack[0]) (see stack). For example, below macros evaluate _that.name vs _this.name on #MindPage:

#MindPage/core/properties/_stack returns the current evaluation stack. For example, below is a macro that evaluates _stack on #MindPage:
<<_item('#MindPage').eval('json(_stack.map((id)=>_item(id).name))')>>`

#macros/graph #_graphviz macro generates a graph using #graphviz.

function graph(dot, style) {
  return html(_item('$id').read('html') // from html block (edit to see)
          .replaceAll('%dot', dot)
          .replaceAll('%style', style))
}
&lt;!-- *_removed block is not included in rendered item -->
&lt;!-- $cid is a unique content id based on macro usage -->
&lt;div id="graph-$cid" class="dot" style="%style">&lt;script>
graphviz._graph('#graph-$cid', `%dot`)
&lt;/script>&lt;/div>

#macros/bar_chart macro generates a simple bar chart using #c3.

function bar_chart(data, style='', bar_colors={}) {
  if (data=="") return ""
  return html(_item('$id').read('html') // from html block (edit to see)
    .replaceAll('%data', typeof data == 'string' ? 
      data : JSON.stringify(data))
    .replaceAll('%style', style)
    .replaceAll('%bar_colors', JSON.stringify(bar_colors)))
}
&lt;!-- *_removed block is not included in rendered item -->
&lt;!-- $cid is a unique content identifier based on macro usage -->
&lt;!-- c3._chart is defined in item #c3 referenced/imported above -->
&lt;div id="bar_chart-$cid" class="c3" style="%style">&lt;script>
const data = JSON.parse(`%data`)
const columns = Array.isArray(data) ? data :
  [['x',..._.keys(data)], ['y',..._.values(data)]]
const x = columns[0][0] == 'x' ? 'x' : ''
c3._chart('#bar_chart-$cid', {
  data: { columns, x, type:'bar', colors:JSON.parse(`%bar_colors`) },
  axis: { 
    rotated:true, 
    x: { type:'category', tick: { multiline:false } },
    y: { show: false } // visible in tooltips
  },
  padding: { bottom: 7 }, // helps center rotated bar charts
  legend: { show: (columns.length - (x?1:0) > 1) }
})
&lt;/script>&lt;/div>

#MindPage/core/properties/_user is the current user:
<<json(_user)>>

#MindPage/core/properties/_this is the currently evaluating item at the top of the evaluation stack, equivalent to _item(_.last(_stack)) (see stack). For example, below is the macro \&lt;&lt;json(_this)>> evaluated as this item is rendered:
<<json(_this)>>

#flat names are not nested.

#log This is an example log item. Editing this item will not move it up in time.

#more types of content is possible using #preloaded and dynamically loaded libraries such as #mathjs, #jstat, #tensorflow, #webppl, ...

#webppl/summarize #_code/util outputs most likely (discrete) values and their probabilities in in a format suitable for charting, e.g. using #macros/bar_chart.

function webppl_summarize(dist, prior=null, limit=10, format=".2f") {
  dist = dist.getDist() // get value:prob object
  const vJ = _.keys(dist).map((v) => v.toString())
  const pJ = _.values(dist).map((p) => p.prob).map(d3.format(format))
  let jJ = _.sortBy(_.range(pJ.length), (j) => -pJ[j])
  jJ = _.take(jJ.filter((j) => pJ[j] > 0), limit)
  let columns = _.zip(['x','p'], ...jJ.map((j)=>[vJ[j],pJ[j]]))
  if (prior) {
    prior = prior?.getDist()
    const qJ = jJ.map((j) => prior[vJ[j]]?.prob || "0")
    columns.push(['prior', ...qJ.map(d3.format(format))])
    columns[1][0] = 'posterior'
  }
  return columns
}

#text/headings

Heading

Heading

Heading

...

#preloaded libraries are #lodash, #mathjax, #c3, #d3, and #graphviz. You can #load other libraries as needed.

#code/util

// from https://stackoverflow.com/a/175787
function isNumeric(str) {
  if (typeof str != "string") return false
  return !isNaN(str) && // require _entire_ string to be numeric
         !isNaN(parseFloat(str)) // handle whitespace-only strings
}

#lodash is a utility library #preloaded as _ (or window._)

#mathjs #_load #_async is a math library. See #mathjs/example.

if (!window.math) {
  await _load('https://cdn.jsdelivr.net/npm' +
    '/mathjs@9.4.4/lib/browser/math.min.js')
}

#d3 is a data visualization library #preloaded as d3 (or window.d3).

#mathjax is a math rendering library #preloaded as MathJax (or window.MathJax).

#text/emphasis can be italic, bold, italic bold, or monospaced.

#text/lists

  1. or ordered
  2. with automatic numbering
    1. and nesting