Revert "presentation: publishable Reveal.js root — index.html at presentation root with dist/ and plugin/ (copy of deck/ adjusted), per remote deployment by clone; deck/ stays the working source"
This reverts commit d54f1ca8b5.
This commit is contained in:
@@ -1,385 +0,0 @@
|
||||
/* AritmoLab deck theme — consolidated light layout (variant 1) */
|
||||
:root {
|
||||
--bordeaux: #cb333b; --bordeaux-hover: #b7252d; --bordeaux-deep: #8f1204;
|
||||
--bordeaux-tint: #f4dfe1;
|
||||
--surface: #fdfcfc; --surface-alt: #f6f5f5; --surface-sunken: #ecebeb;
|
||||
--ink: #2a2426; --ink-muted: #6b6568;
|
||||
--line: #e3e0e1; --line-strong: #c9c5c6;
|
||||
--sans: 'Source Sans 3', system-ui, sans-serif;
|
||||
--display: 'Source Serif 4', Georgia, serif;
|
||||
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.reveal { font-family: var(--sans); }
|
||||
.reveal .slides { text-align: left; }
|
||||
.reveal .slides section.arit {
|
||||
width: 100%; height: 100%; box-sizing: border-box;
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--surface); color: var(--ink);
|
||||
text-align: left; padding: 0; font-family: var(--sans); font-size: 16px;
|
||||
}
|
||||
.reveal .arit *, .reveal .arit *::before, .reveal .arit *::after { box-sizing: border-box; }
|
||||
|
||||
/* ---------- chrome ---------- */
|
||||
.arit .head {
|
||||
flex: 0 0 104px; height: 104px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 40px; background: var(--surface); border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.arit .head img { height: 80px; display: block; }
|
||||
.arit .head-org { font: 700 11.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }
|
||||
.arit .band {
|
||||
height: 46px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 40px; background: var(--bordeaux); color: var(--surface); flex: 0 0 46px;
|
||||
}
|
||||
.arit .band-section { font: 700 13px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; }
|
||||
.arit .band select.jump {
|
||||
background: transparent; color: #fff; border: none;
|
||||
padding: 6px 12px; font: 700 11.5px/1.2 var(--sans);
|
||||
letter-spacing: .04em; cursor: pointer;
|
||||
}
|
||||
.arit .band select.jump option { color: var(--ink); background: #fff; }
|
||||
.arit .foot {
|
||||
flex: 0 0 34px; height: 34px; display: flex; align-items: center; justify-content: space-between; gap: 24px;
|
||||
padding: 0 40px; border-top: 1px solid var(--line);
|
||||
font: 600 10px/1.4 var(--sans); letter-spacing: .03em; color: var(--ink-muted);
|
||||
}
|
||||
.arit .foot span { white-space: nowrap; flex-shrink: 0; }
|
||||
.arit .foot span:first-child { overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; min-width: 0; }
|
||||
.arit .foot span:first-child { overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; }
|
||||
.arit .sbody { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 30px 48px 22px; text-align: left; }
|
||||
.arit.title-slide .sbody { justify-content: center; }
|
||||
.arit.center-v .sbody { justify-content: center; }
|
||||
|
||||
/* ---------- typography ---------- */
|
||||
.arit .kicker { font: 700 12px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 14px; }
|
||||
.arit h1 { font: 700 46px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0; }
|
||||
.arit h2 { font: 700 30px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0 0 8px; }
|
||||
.arit .lead { font: 400 16px/1.5 var(--sans); color: var(--ink-muted); margin: 8px 0 0; }
|
||||
.arit.title-slide .lead { font-size: 19px; text-align: center; }
|
||||
.arit .hgap { height: 16px; flex: 0 0 16px; }
|
||||
|
||||
/* title slide */
|
||||
.arit .title-meta { display: flex; align-items: center; gap: 14px; margin-top: 28px; }
|
||||
.arit .title-meta .pill {
|
||||
display: inline-flex; align-items: center; height: 34px; padding: 0 18px;
|
||||
border-radius: 9999px; background: var(--bordeaux); color: #fdfcfc;
|
||||
font: 700 12.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase;
|
||||
}
|
||||
.arit .title-meta .what { font: 400 14.5px/1.5 var(--sans); color: var(--ink-muted); }
|
||||
.arit .speakers { margin-top: 22px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.arit .speakers span { font: 600 15.5px/1.4 var(--sans); color: var(--ink); }
|
||||
.arit .event { margin: 16px 0 0; font: italic 400 15px/1.5 var(--display); color: var(--ink-muted); }
|
||||
.arit .event-where { margin: 8px 0 0; font: 600 14px/1.4 var(--sans); color: var(--ink-muted); }
|
||||
|
||||
/* quote */
|
||||
.arit .quote {
|
||||
position: relative; margin: 14px 0 20px; padding: 20px 26px 18px 68px; border: 1px solid var(--line);
|
||||
background: var(--surface-alt); border-radius: 8px;
|
||||
font: italic 400 19px/1.55 var(--display); color: var(--ink);
|
||||
}
|
||||
.arit .quote::before { content: '\201C'; position: absolute; left: 22px; top: 10px;
|
||||
font: 700 52px/1 var(--display); color: var(--bordeaux); }
|
||||
|
||||
/* EN translations alongside Italian clinical text */
|
||||
.arit .tr-grid { display: grid; grid-template-columns: 1.12fr 1fr; gap: 24px; align-items: center; }
|
||||
.arit .tr-grid .it, .arit .tr-grid .en { margin: 0; }
|
||||
.arit .tr-grid .en { padding-left: 20px; border-left: 1px solid var(--line-strong);
|
||||
font: 400 19px/1.5 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .en-tag { display: inline-block; margin-right: 8px; padding: 2.5px 8px; border-radius: 4px;
|
||||
background: var(--bordeaux); color: #fdfcfc; font: 700 9.5px/1 var(--sans); letter-spacing: .12em;
|
||||
vertical-align: 2px; }
|
||||
.arit .points .g { color: var(--bordeaux-deep); }
|
||||
.arit .chipc .g { font-weight: 400; opacity: .78; }
|
||||
.arit .code .en { font: 400 13px/1.5 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .code .en::before { content: 'EN'; display: inline-block; margin-right: 8px; padding: 2px 6px;
|
||||
border-radius: 3px; background: var(--bordeaux); color: #fdfcfc;
|
||||
font: 700 8.5px/1 var(--sans); letter-spacing: .1em; }
|
||||
|
||||
/* ---------- slide "The problem": challenges + outputs rail + ThothII right ---------- */
|
||||
.arit.problem .sbody { padding-top: 20px; padding-bottom: 14px; }
|
||||
.arit.problem .kicker { margin-bottom: 10px; }
|
||||
.arit.problem .quote { margin: 0 0 10px; padding: 12px 22px 10px 64px; font-size: 16.5px; }
|
||||
.arit.problem .tr-grid .en { font-size: 16.5px; }
|
||||
.arit.problem .stage { padding: 11px 10px; }
|
||||
.arit.problem .stage b { font-size: 13px; text-transform: uppercase; }
|
||||
.arit.problem .stage span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .02em; }
|
||||
.arit.problem .stage.hot { background: #fdf1f2; border-color: var(--bordeaux); }
|
||||
.arit .brain-ai { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
|
||||
border-radius: 9999px; background: #fff; border: 1.5px solid var(--bordeaux); }
|
||||
.arit .brain-ai svg { width: 24px; height: 24px; color: var(--bordeaux); }
|
||||
.arit.problem .stage span.brain-ai { display: inline-flex; margin: 3px auto 0; }
|
||||
.arit.problem .midrow { display: flex; gap: 0; margin-top: 8px; align-items: stretch; }
|
||||
.arit.problem .concepts { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.arit.problem .concepts .citem { flex: 1; }
|
||||
.arit .concepts .lbl, .arit .rail .lbl { display: block; font: 700 10px/1 var(--mono); letter-spacing: .14em;
|
||||
text-transform: uppercase; color: var(--bordeaux); margin: 0 0 5px; }
|
||||
.arit .rail .lbl { padding-left: 16px; }
|
||||
.arit .citem { display: flex; gap: 12px; align-items: baseline; padding: 5px 0; border-bottom: 1px solid var(--line); }
|
||||
.arit .citem:last-child { border-bottom: none; }
|
||||
.arit .citem .n { font: 700 11px/1 var(--mono); color: var(--bordeaux); }
|
||||
.arit .citem b { font: 700 12.5px/1.3 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
|
||||
.arit .citem span.d { font: 400 11px/1.4 var(--sans); color: var(--ink-muted); margin-left: 8px; }
|
||||
.arit .railwrap { position: relative; display: flex; justify-content: flex-end; }
|
||||
.arit .conn-drop, .arit .t-conn { position: absolute; width: 2px; background: var(--bordeaux); }
|
||||
.arit .conn-elbow { position: absolute; height: 2px; background: var(--bordeaux); }
|
||||
.arit .t-head { position: absolute; width: 0; height: 0; border: 5px solid transparent;
|
||||
border-bottom: 7px solid var(--bordeaux); transform: translateX(-50%); }
|
||||
.arit .rail { width: 540px; border-left: 2px solid var(--bordeaux); padding-top: 12px; }
|
||||
.arit .rail .row { display: flex; align-items: center; gap: 12px; padding: 5px 14px 5px 16px; position: relative; }
|
||||
.arit .rail .row::before { content: ''; position: absolute; left: -2px; top: 50%; width: 14px; height: 2px; background: var(--bordeaux); }
|
||||
.arit .rail .row .oicon { flex: 0 0 26px; width: 26px; height: 26px; color: var(--bordeaux); display: block; }
|
||||
.arit .rail .row b { font: 700 13px/1.2 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
|
||||
.arit .rail .row span.d { font: 400 11px/1.3 var(--sans); color: var(--ink-muted); }
|
||||
.arit.problem .thoth-wrap { position: relative; justify-content: flex-end; margin-top: 10px; }
|
||||
.arit.problem .thoth-box { padding: 8px 14px; font-size: 11px; text-transform: uppercase; letter-spacing: .02em; }
|
||||
.arit.problem .thoth-box .brain-ai { margin-right: 9px; vertical-align: middle; }
|
||||
.arit .quote small { display: block; margin-top: 6px; font: 600 11px/1.4 var(--sans); font-style: normal;
|
||||
letter-spacing: .06em; text-transform: uppercase; color: var(--ink-muted); }
|
||||
|
||||
/* mini pipeline (problem / thoth slides) */
|
||||
.arit .pipeline { display: flex; align-items: stretch; gap: 12px; }
|
||||
.arit .stage { flex: 1; padding: 15px 12px; text-align: center; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-alt); }
|
||||
.arit .stage b { display: block; font: 700 14px/1.2 var(--sans); margin-bottom: 4px; }
|
||||
.arit .stage span { display: block; font: 400 11.5px/1.35 var(--sans); color: var(--ink-muted); }
|
||||
.arit .stage .tag { display: inline-block; margin-top: 6px; padding: 3px 10px; border-radius: 9999px;
|
||||
background: var(--bordeaux); color: #fdfcfc; font: 700 9.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; }
|
||||
.arit .stage.ai { background: var(--bordeaux-tint); border-color: var(--bordeaux); }
|
||||
.arit .arrow { align-self: center; color: var(--line-strong); font: 700 20px/1 var(--sans); }
|
||||
.arit .thoth-wrap { display: flex; justify-content: flex-end; margin-top: 18px; }
|
||||
.arit .thoth-col { width: 540px; text-align: center; }
|
||||
.arit .thoth-arrow { color: var(--line-strong); font: 700 18px/1 var(--sans); margin: 2px 0; }
|
||||
.arit .thoth-box { border: 1px solid var(--bordeaux); border-radius: 8px; padding: 12px 16px;
|
||||
background: var(--surface-alt); font: 600 13px/1.4 var(--sans); }
|
||||
.arit .thoth-box .tag { margin-right: 8px; }
|
||||
|
||||
/* points */
|
||||
.arit .points { list-style: none; margin: 0; padding: 0; }
|
||||
.arit .points li { position: relative; padding: 11px 0 11px 26px; border-bottom: 1px solid var(--line);
|
||||
font: 400 15px/1.45 var(--sans); color: var(--ink); }
|
||||
.arit .points li::before { content: ''; position: absolute; left: 0; top: 18px;
|
||||
width: 10px; height: 10px; border-radius: 9999px; background: var(--bordeaux); }
|
||||
.arit .points code, .arit .stat-note code, .arit .ai-pop-list code {
|
||||
font: 600 0.92em var(--mono); color: var(--bordeaux-deep); background: var(--surface-alt);
|
||||
padding: 1px 5px; border-radius: 4px; }
|
||||
|
||||
/* split + code panel */
|
||||
.arit .split { display: flex; gap: 40px; flex: 1; min-height: 0; }
|
||||
.arit .split .left { flex: 0 0 430px; display: flex; flex-direction: column; }
|
||||
.arit .split .right { flex: 1; min-width: 0; display: flex; }
|
||||
.arit.nlp .split .right { align-items: center; }
|
||||
.arit.nlp .code { flex: 0 0 auto; }
|
||||
.arit .code { width: 100%; display: flex; flex-direction: column; overflow: hidden;
|
||||
border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface-sunken); }
|
||||
.arit .code-head { flex: 0 0 38px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 16px; border-bottom: 1px solid var(--line); font: 600 11px/1 var(--mono); color: var(--ink-muted); }
|
||||
.arit .code pre { flex: 1; padding: 16px 18px; overflow: hidden; margin: 0;
|
||||
font: 400 13px/1.6 var(--mono); color: var(--ink); text-align: left; box-shadow: none; }
|
||||
.arit .code b { font-weight: 700; color: var(--bordeaux-deep); }
|
||||
.arit .code i { font-style: normal; color: #2f6f4f; }
|
||||
|
||||
/* stats — editorial numeral band */
|
||||
.arit .stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
|
||||
.arit .stat { padding: 8px 24px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.arit .stat:first-child { padding-left: 0; }
|
||||
.arit .stat + .stat { border-left: 1px solid var(--line); }
|
||||
.arit .stat .num { font: 700 62px/1 var(--sans); color: var(--bordeaux); letter-spacing: -.02em; }
|
||||
.arit .stat .lbl { font: 600 14.5px/1.35 var(--sans); color: var(--ink); }
|
||||
.arit .stat .sub { font: 400 12px/1.45 var(--sans); color: var(--ink-muted); }
|
||||
.arit .stat-note { margin-top: 26px; font: 400 13px/1.5 var(--sans); color: var(--ink-muted); }
|
||||
|
||||
/* slide "What the text miner reads": slim numbers strip + elliptical component cycle */
|
||||
.arit.stats .sbody { padding-top: 22px; }
|
||||
.arit.stats .strip { display: flex; justify-content: space-between; align-items: baseline;
|
||||
border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 9px 2px; }
|
||||
.arit.stats .sstat { display: flex; align-items: baseline; gap: 9px; }
|
||||
.arit.stats .sstat .n { font: 700 25px/1 var(--sans); color: var(--bordeaux); letter-spacing: -.02em; }
|
||||
.arit.stats .sstat .l { font: 600 11.5px/1.2 var(--sans); color: var(--ink); }
|
||||
.arit.stats .cycle { position: relative; width: 1184px; height: 270px; margin: 4px auto 0; }
|
||||
.arit.stats .cycle-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.arit.stats .vcomp { position: absolute; transform: translate(-50%, -50%); width: 250px;
|
||||
text-align: center; background: var(--surface); padding: 4px 6px; }
|
||||
.arit.stats .vcomp .cicon { width: 46px; height: 46px; color: var(--bordeaux); display: block; margin: 0 auto 7px; }
|
||||
.arit.stats .vcomp b { display: block; font: 700 14.5px/1.25 var(--sans); text-transform: uppercase;
|
||||
letter-spacing: .04em; color: var(--ink); }
|
||||
.arit.stats .vcomp span.d { display: block; margin-top: 4px; font: 400 11.5px/1.4 var(--sans); color: var(--ink-muted); }
|
||||
.arit.stats .note-row { display: flex; gap: 30px; margin-top: 6px; }
|
||||
.arit.stats .nitem { flex: 1; position: relative; padding-left: 16px; font: 400 14px/1.45 var(--sans); color: var(--ink); }
|
||||
.arit.stats .nitem::before { content: ''; position: absolute; left: 0; top: 7px; width: 8px; height: 8px;
|
||||
border-radius: 9999px; background: var(--bordeaux); }
|
||||
.arit.stats .nitem b { color: var(--bordeaux-deep); }
|
||||
|
||||
/* divider */
|
||||
.arit.divider .sbody { justify-content: center; }
|
||||
.arit .numeral { font: 700 110px/1 var(--sans); color: var(--bordeaux); }
|
||||
.arit .divider-h { font: 700 40px/1.15 var(--sans); margin-top: 10px; }
|
||||
.arit .cue { margin-top: 22px; font: 600 12px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-muted); }
|
||||
|
||||
/* ontology slide: EN-first category pills + output panel */
|
||||
.arit .onto-split { flex: 0 0 auto; display: grid; grid-template-columns: 1.25fr 1fr;
|
||||
gap: 26px; align-items: stretch; margin-top: 14px; }
|
||||
.arit .tier-h { display: flex; align-items: center; font: 700 10px/1 var(--mono); letter-spacing: .14em;
|
||||
text-transform: uppercase; color: var(--bordeaux-deep); margin: 22px 0 9px; }
|
||||
.arit .onto-left .tier-h:first-child { margin-top: 0; }
|
||||
.arit .tier-h .cnt { margin-left: auto; font: 700 9px/1 var(--mono); color: var(--bordeaux);
|
||||
border: 1px solid var(--bordeaux); border-radius: 9999px; padding: 2px 7px; }
|
||||
.arit .cats { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.arit .cat { display: inline-flex; align-items: baseline; gap: 7px; border: 1px solid var(--bordeaux);
|
||||
border-radius: 9999px; padding: 6px 13px; background: #fff; white-space: nowrap; }
|
||||
.arit .cat b { font: 700 13px/1 var(--sans); color: var(--ink); }
|
||||
.arit .cat .it { font: 400 11px/1 var(--sans); color: var(--ink-muted); }
|
||||
.arit .cat.fill { background: var(--bordeaux-tint); }
|
||||
.arit .onto-points { margin-top: 18px; }
|
||||
.arit .onto-left .onto-points:first-child { margin-top: 0; }
|
||||
.arit .onto-left { display: flex; flex-direction: column; }
|
||||
.arit .onto-left .roi { margin-top: auto; }
|
||||
.arit .onto-left .onto-points { margin-bottom: 14px; }
|
||||
.arit .oitem { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); }
|
||||
.arit .oitem:last-child { border-bottom: none; }
|
||||
.arit .oitem .n { flex: 0 0 22px; font: 700 11px/1.6 var(--mono); color: var(--bordeaux); }
|
||||
.arit .oitem b { font: 700 13px/1.35 var(--sans); color: var(--ink); }
|
||||
.arit .oitem .d { font: 400 12.5px/1.45 var(--sans); color: var(--ink-muted); }
|
||||
.arit .out-panel { border: 1px solid var(--bordeaux); background: var(--bordeaux-tint); border-radius: 10px;
|
||||
padding: 15px 18px 8px; align-self: stretch; display: flex; flex-direction: column; }
|
||||
.arit .out-h { display: block; font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--bordeaux-deep); margin-bottom: 10px; }
|
||||
.arit .out-item { display: flex; gap: 10px; padding: 13px 0; border-bottom: 1px solid rgba(143,18,4,.18); flex: 1; }
|
||||
.arit .out-item:last-child { border-bottom: none; }
|
||||
.arit .out-item .n { flex: 0 0 22px; font: 700 11px/1.6 var(--mono); color: var(--bordeaux-deep); }
|
||||
.arit .out-item b { font: 700 13px/1.35 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .out-item .d { display: block; margin-top: 2px; font: 400 12.5px/1.45 var(--sans); color: var(--ink); }
|
||||
|
||||
/* flow */
|
||||
.arit .flow { margin-top: 16px; display: flex; align-items: stretch; gap: 14px; }
|
||||
.arit .zone { border-radius: 8px; padding: 30px 14px 14px; position: relative; }
|
||||
.arit .zone > .zlabel { position: absolute; top: 8px; left: 14px;
|
||||
font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; }
|
||||
.arit .zone.existed { flex: 0 0 168px; background: var(--surface-alt); border: 1px dashed var(--line-strong); }
|
||||
.arit .zone.existed > .zlabel { color: var(--ink-muted); }
|
||||
.arit .zone.built { flex: 1; background: #faf4f5; border: 1px solid #e7c3c6; }
|
||||
.arit .zone.built > .zlabel { color: var(--bordeaux); }
|
||||
.arit .fcol { flex: 1; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
||||
.arit .fbox { background: #fff; border: 1px solid var(--line-strong); border-radius: 6px; padding: 10px; text-align: center; }
|
||||
.arit .fbox b { display: block; font: 700 13.5px/1.25 var(--sans); }
|
||||
.arit .fbox span { display: block; margin-top: 3px; font: 400 11px/1.3 var(--sans); color: var(--ink-muted); }
|
||||
.arit .fbox.src { background: var(--surface-alt); border-color: var(--line); }
|
||||
.arit .fbox.src.future { background: transparent; border: 1.5px dashed var(--line-strong); }
|
||||
.arit .fbox.src.future b { color: var(--ink-muted); font-weight: 700; }
|
||||
.arit .fbox.src.future .plus { display: inline-flex; width: 16px; height: 16px; margin-right: 5px; vertical-align: -3px;
|
||||
align-items: center; justify-content: center; border-radius: 9999px; border: 1.5px solid var(--bordeaux);
|
||||
color: var(--bordeaux); font: 800 11px/1 var(--sans); }
|
||||
.arit .fbox.end { background: var(--bordeaux); border-color: var(--bordeaux); color: #fff; padding: 14px 10px; }
|
||||
.arit .fbox.end span { color: rgba(255,255,255,.85); }
|
||||
.arit .farrow { align-self: center; color: var(--bordeaux); font: 700 15px/1 var(--sans); }
|
||||
.arit .fcol.ai { position: relative; padding-top: 12px; }
|
||||
.arit .brain {
|
||||
position: absolute; top: -6px; right: 6px; z-index: 5;
|
||||
width: 30px; height: 30px; border-radius: 9999px;
|
||||
background: #fff; border: 1.5px solid var(--bordeaux);
|
||||
display: flex; align-items: center; justify-content: center; font-size: 15px;
|
||||
}
|
||||
.arit .fcap { font: italic 400 11px/1.4 var(--sans); color: var(--ink-muted); text-align: center; }
|
||||
.arit .fcol.ai-hit { flex: 0 0 44px; align-items: center; justify-content: center; gap: 8px; }
|
||||
.arit .fcol.ai-hit .brain-btn { position: static; }
|
||||
|
||||
/* clickable brains */
|
||||
button.brain-btn { cursor: pointer; padding: 0; transition: transform .15s ease, box-shadow .15s ease; }
|
||||
button.brain-btn:hover, button.brain-btn:focus-visible { transform: scale(1.18); box-shadow: 0 0 0 5px rgba(203,51,59,.14); outline: none; }
|
||||
|
||||
/* popup */
|
||||
.ai-pop { position: fixed; inset: 0; z-index: 100; background: rgba(42,36,38,.45); backdrop-filter: blur(2px);
|
||||
display: block; opacity: 0; pointer-events: none; transition: opacity .2s ease; }
|
||||
.ai-pop.open { opacity: 1; pointer-events: auto; }
|
||||
.ai-line { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.ai-line line { stroke: var(--bordeaux); stroke-width: 2.5; }
|
||||
.ai-line circle { fill: var(--bordeaux); }
|
||||
.ai-pop-card { position: absolute; width: min(580px, 92vw); background: #fff; border: 1px solid var(--line);
|
||||
border-radius: 12px; box-shadow: 0 18px 50px rgba(42,36,38,.28); padding: 24px 28px 22px;
|
||||
transform: translateY(14px) scale(.95); transition: transform .24s cubic-bezier(.2,.9,.3,1.15); text-align: left; }
|
||||
.ai-pop.open .ai-pop-card { transform: none; }
|
||||
.ai-pop-kicker { font: 700 10.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 10px; }
|
||||
.ai-pop-title { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.ai-pop-title .brain { position: static; }
|
||||
.ai-pop-title .brain-btn { cursor: default; pointer-events: none; }
|
||||
.ai-pop-title b { font: 700 18px/1.3 var(--sans); color: var(--ink); font-family: var(--sans); }
|
||||
.ai-pop-card p { font: 400 14px/1.65 var(--sans); color: var(--ink); margin: 0; text-align: left; }
|
||||
.ai-pop-card p.lead { margin-bottom: 14px; }
|
||||
.ai-pop-list { list-style: none; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; }
|
||||
.ai-pop-list li { position: relative; padding-left: 20px; font: 400 13.5px/1.5 var(--sans); color: var(--ink); }
|
||||
.ai-pop-list li::before { content: ''; position: absolute; left: 0; top: 7px; width: 8px; height: 8px; border-radius: 9999px; background: var(--bordeaux); }
|
||||
.ai-pop-list b { font-weight: 800; }
|
||||
.ai-pop-list .n { color: var(--bordeaux-deep); font-weight: 800; }
|
||||
.roi { margin-top: 14px; border: 1.5px solid var(--bordeaux); background: #faf4f5; border-radius: 8px;
|
||||
padding: 10px 14px; font: 400 13.5px/1.55 var(--sans); color: var(--ink); }
|
||||
.roi-quiet { border-color: var(--line-strong); border-width: 1px; background: var(--surface-alt); }
|
||||
.roi .k { display: block; margin-bottom: 4px; font: 800 10.5px/1 var(--mono); letter-spacing: .14em;
|
||||
text-transform: uppercase; color: var(--bordeaux-deep); }
|
||||
.ai-pop-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border: none;
|
||||
border-radius: 9999px; background: var(--surface-alt); font: 700 15px/1 var(--sans); color: var(--ink-muted); cursor: pointer; }
|
||||
.ai-pop-x:hover { background: var(--bordeaux-tint); color: var(--bordeaux-deep); }
|
||||
|
||||
/* chips on slides (wording TBD) */
|
||||
.arit .chip.tbd { color: var(--bordeaux-deep); border: 1px solid var(--bordeaux); background: #fff;
|
||||
border-radius: 9999px; padding: 6px 12px; font: 700 10.5px/1 var(--sans); letter-spacing: .06em; }
|
||||
|
||||
/* where we started — scattered orbit map */
|
||||
.arit .scatter { position: relative; flex: 1; min-height: 0; }
|
||||
.arit .todo-stack { position: absolute; z-index: 2; }
|
||||
.arit .todo { display: flex; align-items: center; gap: 12px; border: 2px dashed var(--bordeaux);
|
||||
border-radius: 8px; background: #fff; padding: 8px 12px; margin-bottom: 8px;
|
||||
box-shadow: 0 2px 8px rgba(42,36,38,.06); cursor: pointer; }
|
||||
.arit .todo .q { font: 800 22px/1 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .todo .tx { display: flex; flex-direction: column; }
|
||||
.arit .todo b { font: 700 13.5px/1.3 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .todo .tx span { font: 400 12px/1.4 var(--sans); color: var(--ink-muted); margin-top: 2px; }
|
||||
.arit .missing-title { position: absolute; font: 700 30px/1.2 var(--sans); color: var(--bordeaux-deep); }
|
||||
.arit .isle { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 2px;
|
||||
cursor: pointer; transform: rotate(var(--rot, 0deg)); transition: transform .18s ease;
|
||||
border: none; background: none; font-family: var(--sans); width: 230px; }
|
||||
.arit .isle:hover, .arit .isle:focus-visible { transform: rotate(var(--rot, 0deg)) scale(1.05); outline: none; }
|
||||
.arit .isle .ico { width: var(--size, 120px); height: var(--size, 120px); color: var(--bordeaux); display: block; }
|
||||
.arit .isle .nm { font: 700 16px/1.3 var(--sans); color: var(--ink); margin-top: 4px; }
|
||||
.arit .isle .sc { font: 400 13.5px/1.4 var(--sans); color: var(--ink-muted); max-width: 240px; }
|
||||
.arit .hint { margin-top: 12px; font: italic 400 12px/1.4 var(--sans); color: var(--ink-muted); }
|
||||
|
||||
/* snapshot tours + chart placeholders */
|
||||
.arit .shots { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: 1fr 1fr;
|
||||
gap: 14px; flex: 1; min-height: 0; align-content: center; }
|
||||
.arit .shot { border: 1.5px dashed var(--line-strong); border-radius: 8px;
|
||||
background: var(--surface-alt); display: flex; align-items: center; justify-content: center;
|
||||
font: italic 400 12px/1.4 var(--sans); color: var(--ink-muted); min-height: 0; overflow: hidden; }
|
||||
.arit .charts { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; flex: 1; min-height: 0; align-content: center; }
|
||||
.arit .chart { border: 1.5px solid var(--line); border-radius: 8px; background: #fff; min-height: 0;
|
||||
display: flex; flex-direction: column; overflow: hidden; }
|
||||
.arit .chart b { display: block; padding: 12px 16px; font: 700 14px/1.3 var(--sans); color: var(--ink);
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.arit .chart span { flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
font: italic 400 12px/1.4 var(--sans); color: var(--ink-muted); text-align: center; padding: 0 20px; }
|
||||
|
||||
/* pillars strip (vision slide) */
|
||||
.arit .pillars { margin-top: auto; padding-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.arit .pillars .agents { font: italic 600 12px/1.4 var(--sans); color: var(--bordeaux-deep);
|
||||
border: 1.5px dashed var(--bordeaux); border-radius: 8px; padding: 7px 12px; background: #faf4f5; }
|
||||
.arit .pillars .pillar { border: 1px solid var(--line-strong); border-radius: 9999px;
|
||||
padding: 6px 12px; font: 600 11.5px/1 var(--sans); color: var(--ink-muted); }
|
||||
.arit .pillars .pillar b { color: var(--ink); font-weight: 700; }
|
||||
|
||||
/* Reveal sets inline "display: block" on the presented section — beat it
|
||||
(author !important wins over inline) so our flex column layout applies */
|
||||
.reveal .slides section.arit.present { display: flex !important; }
|
||||
|
||||
/* hide reveal default noise on our chrome */
|
||||
.reveal .slides section.arit aside.notes { display: none; }
|
||||
|
||||
/* vision slide: tighter vertical rhythm to keep pillars clear of the footer */
|
||||
.arit.flow-slide .kicker { margin-bottom: 10px; }
|
||||
.arit.flow-slide h2 { margin-bottom: 6px; }
|
||||
.arit.flow-slide .lead { margin-top: 6px; font-size: 15px; }
|
||||
.arit.flow-slide .flow { margin-top: 10px; }
|
||||
.arit.flow-slide .zone { padding: 26px 14px 12px; }
|
||||
.arit.flow-slide .fbox { padding: 8px 10px; }
|
||||
.arit.flow-slide .fbox.end { padding: 12px 10px; }
|
||||
.arit.flow-slide .fcol { gap: 6px; }
|
||||
.arit.flow-slide .pillars { padding-top: 8px; }
|
||||
|
||||
/* popup: lead + list (missing-pieces popups) */
|
||||
.ai-pop-card p.lead { font-style: italic; color: var(--ink-muted); }
|
||||
Vendored
-8
File diff suppressed because one or more lines are too long
Vendored
-9
File diff suppressed because one or more lines are too long
@@ -1,11 +0,0 @@
|
||||
/* Embedded fonts — Source Sans 3 & Source Serif 4 (SIL Open Font License) */
|
||||
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 400; font-display: swap;
|
||||
src: url('fonts/ss3-400.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 600; font-display: swap;
|
||||
src: url('fonts/ss3-600.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Sans 3'; font-style: normal; font-weight: 700; font-display: swap;
|
||||
src: url('fonts/ss3-700.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Serif 4'; font-style: normal; font-weight: 400; font-display: swap;
|
||||
src: url('fonts/ss4-400.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Serif 4'; font-style: italic; font-weight: 400; font-display: swap;
|
||||
src: url('fonts/ss4-italic-400.woff2') format('woff2'); }
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,826 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AritmoLab — Role of AI in the Analysis of Unstructured Clinical Databases</title>
|
||||
<link rel="stylesheet" href="dist/reveal.css">
|
||||
<link rel="stylesheet" href="fonts.css?v=8">
|
||||
<link rel="stylesheet" href="deck.css?v=40">
|
||||
</head>
|
||||
<body>
|
||||
<div class="reveal"><div class="slides">
|
||||
|
||||
|
||||
<section class="arit title-slide">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="Policlinico San Donato">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">AritmoLab · Clinical Data Platform</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<h1>Role of AI in the Analysis of Unstructured Clinical Databases</h1>
|
||||
<div class="hgap"></div>
|
||||
<p class="lead">How we turned a mix of structured and free-text clinical records into a research data platform — the AritmoLab platform experience.</p>
|
||||
<div class="title-meta">
|
||||
<span class="pill">AritmoLab</span>
|
||||
<span class="what">A clinical data platform hosting cardiology data and analysis tools</span>
|
||||
</div>
|
||||
<div class="speakers">
|
||||
<span>Dr. Marco Pancotti - MultiPhysixLab</span>
|
||||
<span>Dr. Sara Paratico - I.R.C.C.S. Policlinico San Donato</span>
|
||||
</div>
|
||||
<p class="event">“Multidimensional Characterization of Cardiac Arrhythmias: Role of Electrocardiology in the Artificial Intelligence Era”</p>
|
||||
<p class="event-where">San Donato Milanese, Milan, Italy · 2–3 October 2026</p>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">01 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
Good morning everyone, and thank you for being here. I'm Marco Pancotti, and with my colleague Dr. Sara Paratico we work on the clinical data platform of AritmoLab at Policlinico San Donato.
|
||||
Today I want to talk about a problem that every hospital knows very well. The most valuable clinical information we have — the diagnosis, the patient's history, the outcome of a test — is written in plain free text, inside systems that were never designed to make that text usable.
|
||||
Over the next twelve minutes I'll show you how we used artificial intelligence, in three different roles, to transform twenty years of cardiology records into a database that researchers can actually query — and that clinicians can verify. My colleague Dr. Sara Paratico will join me to show how we read the clinical text itself.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Where we started</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<h2>Four islands and four missing pieces</h2>
|
||||
<div class="scatter">
|
||||
<div class="todo-stack" style="left:30%; top:22%; width:40%">
|
||||
<div class="todo" data-missing="ci" style="margin-left:0"><span class="q">?</span><span class="tx"><b>Clinical Intelligence</b><span>dashboards on the clinical history of the Unit and its patients</span></span></div>
|
||||
<div class="todo" data-missing="dwh" style="margin-left:9%"><span class="q">?</span><span class="tx"><b>Datawarehouse</b><span>the single source of truth</span></span></div>
|
||||
<div class="todo" data-missing="ml" style="margin-left:4%"><span class="q">?</span><span class="tx"><b>ML-ready data</b><span>cohort tables, ready for model training</span></span></div>
|
||||
<div class="todo" data-missing="portal" style="margin-left:12%"><span class="q">?</span><span class="tx"><b>Professional Portal</b><span>AritmoLab — the grown-up Omics Portal</span></span></div>
|
||||
</div>
|
||||
<button class="isle" data-start="cardioref" style="--rot:-3deg; --size:114px; left:calc(1% + 39px); top:4%; width:230px">
|
||||
<svg class="ico" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M24 33 c-6-3.5-9-7-9-10.5 0-3 4-5.5 7-3 1 .8 1.6 1.8 2 3 .4-1.2 1-2.2 2-3 3-2.5 7 0 7 3 0 3.5-3 7-9 10.5z" fill="currentColor" stroke="none"/><path d="M22.5 21 h3 v3 h3 v3 h-3 v3 h-3 v-3 h-3 v-3 h3 z" fill="#fff" stroke="none"/></svg>
|
||||
<span class="nm">Cardioref</span><span class="sc" style="width:300px">visits · operations · reports — twenty years of management on structured forms</span></button>
|
||||
<button class="isle" data-start="genetic" style="--rot:3deg; --size:114px; left:calc(73% + 39px); top:4%; width:230px">
|
||||
<svg class="ico" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8 C28 14 28 20 18 26 C12 30 12 35 18 40"/><path d="M30 8 C20 14 20 20 30 26 C36 30 36 35 30 40"/><path d="M17 12 H31"/><path d="M15.5 18 H32.5"/><path d="M17 24 H31"/><path d="M14.5 31 H22"/><rect x="25" y="29" width="12" height="12" rx="1.5" stroke-width="2"/><path d="M25 34 H37"/><path d="M30.5 29 V41"/></svg>
|
||||
<span class="nm">Genetic data</span><span class="sc">DNA instruments — results typed into Excel by hand</span></button>
|
||||
<button class="isle" data-start="omics" style="--rot:-2deg; --size:114px; left:calc(1% + 39px); top:55%; width:230px">
|
||||
<svg class="ico" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="24" cy="24" r="20" stroke-width="2.6"/><circle cx="24" cy="24" r="4" fill="currentColor" stroke="none"/><circle cx="24" cy="10" r="3.4"/><circle cx="11.5" cy="31" r="3.4"/><circle cx="36.5" cy="31" r="3.4"/><path d="M24 20.5 V13.5"/><path d="M21 26.5 L14 29.5"/><path d="M27 26.5 L34 29.5"/></svg>
|
||||
<span class="nm">Omics Portal</span><span class="sc">meant to host Cardioref + genetics — still immature</span></button>
|
||||
<button class="isle" data-start="ecg" style="--rot:2deg; --size:114px; left:calc(73% + 39px); top:55%; width:230px">
|
||||
<svg class="ico" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="24" cy="24" r="20" stroke-width="2.6"/><path d="M9 24 h6 l3-9 5 16 3-7 h14" stroke-width="2.4"/><circle cx="24" cy="24" r="1.8" fill="currentColor" stroke="none"/></svg>
|
||||
<span class="nm">ECG</span><span class="sc">paper strip + a CSV on request</span></button>
|
||||
<div class="missing-title" style="left:calc(30% + 14px); top:calc(22% - 46px); width:40%">Missing Pieces</div>
|
||||
<p class="hint" style="position:absolute; left:26%; width:48%; bottom:2px; margin:0; text-align:center">▸ click a system: how it was used — and what held it back · click a missing piece: what it would have given</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">02 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
Before I show you what we built, let me take you back twenty years. Cardioref, our electrophysiology database, had twenty years of clinical records: as a management tool for patients, daily activity and planning it was — and still is — excellent. But for research it was almost unusable, because most of the clinical knowledge was written in free text, outside the structured fields. The genetic data lived in its own system, which produced Excel sheets, with no connection to Cardioref. Our first Omics Portal was a promising idea, still embryonic and not integrated with anything. And a constellation of ECG subsystems that could only hand you a CSV file, on request. On top of that: no clinical intelligence on the historical data, no single datawarehouse as the source of truth, no ML-ready data for predictive statistics, and no professional portal to bring it all together. Four islands, four missing pieces. Click any system and I'll show you exactly what held it back. And here is what we built.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit flow-slide">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">The project</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">The plan</div>
|
||||
<h2>What we wanted to build</h2>
|
||||
<p class="lead">One platform, two destinations: the 360° patient portal and a research-ready warehouse. Built on open-source pillars, with AI coding agents wherever they helped. This talk follows the hardest piece — the unstructured data.</p>
|
||||
<div class="flow">
|
||||
<div class="zone existed">
|
||||
<span class="zlabel">What existed</span>
|
||||
<div class="fcol">
|
||||
<div class="fbox src"><b>Cardioref</b><span>cardiology records · procedures · letters</span></div>
|
||||
<div class="fbox src"><b>Genetic data</b><span>labs · variants · nomenclature</span></div>
|
||||
<div class="fbox src"><b>ECG</b><span>signals · device follow-up</span></div>
|
||||
<div class="fbox src future"><b><span class="plus">+</span>Future sources</b><span>new subsystems can be connected as sources</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcol ai-hit">
|
||||
<button class="brain brain-btn" data-ai="ingestion" aria-label="AI contribution: the mappings">🧠</button>
|
||||
<div class="farrow">→</div>
|
||||
</div>
|
||||
<div class="zone built">
|
||||
<span class="zlabel">What we built</span>
|
||||
<div style="display:flex; align-items:center; gap:10px; height:100%;">
|
||||
<div class="fcol" style="flex:0.8; justify-content:center"><div class="fbox"><b>Staging</b><span>raw replica of the sources</span></div></div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fcol ai" style="flex:1.25">
|
||||
<button class="brain brain-btn" data-ai="integration" aria-label="AI contribution: clinical text reading">🧠</button>
|
||||
<div class="fbox"><b>Integration</b><span>cleaned, normalized, deduplicated</span></div>
|
||||
<div class="fcap">AI reads the clinical text: pathologies, procedures, drug-challenge outcomes</div>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fcol" style="flex:1.05; justify-content:center"><div class="fbox"><b>Star schema</b><span>dimensional transformation</span></div></div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fcol ai" style="flex:1.25">
|
||||
<button class="brain brain-btn" data-ai="datamarts" aria-label="AI contribution: datamart generation">🧠</button>
|
||||
<div class="fbox"><b>Datamarts</b><span>research-ready marts</span></div>
|
||||
<div class="fcap">AI builds them on demand from plain-English questions (ThothII)</div>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fcol" style="flex:1">
|
||||
<div class="fbox end"><b>AritmoLab Data Warehouse</b><span>queried for research</span></div>
|
||||
<div class="fbox end"><b>AritmoLab Portal</b><span>management & exploration</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pillars">
|
||||
<span class="agents">AI coding agents — Anthropic · OpenAI — used wherever they helped, 360° in the code</span>
|
||||
<span class="pillar"><b>Airflow</b> · orchestration</span>
|
||||
<span class="pillar"><b>Superset</b> · dashboards</span>
|
||||
<span class="pillar"><b>Django</b> · portal scaffolding</span>
|
||||
<span class="pillar"><b>Authentik</b> · auth — GSD LDAP</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">03 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
Here is the whole project on one slide. On the left, what already existed: three hospital data sources — Cardioref, our electrophysiology records; the genetic data coming from the labs; and the ECG signals. On the right, what we built in AritmoLab: a staging replica, an integration layer where the data is cleaned and normalized, a star-schema warehouse, and the research datamarts — delivered through the data warehouse and a management portal.
|
||||
Everywhere you see the brain symbol, that's where AI works for us — click each brain and its contribution card opens, tied to the brain by a red line. At integration, AI reads the clinical text. At the end, AI builds the datamarts on demand from plain-English questions. And the mappings that bring the sources in were themselves drafted by AI, then revised by humans.
|
||||
That was the plan. And to build it, we used AI everywhere it helped — drafting configs, reading clinical text, and at the very end, producing the datamarts themselves. This talk follows the hardest piece of that plan: the unstructured data. My colleague Dr. Paratico will show you exactly how the text reading works. But first, the first step of the climb: the mappings.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit center-v problem">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">The problem</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">From free text to a research platform</div>
|
||||
<h2>The clinical truth lives in unstructured columns</h2>
|
||||
<div class="quote">
|
||||
<div class="tr-grid">
|
||||
<p class="it">“Il paziente riferisce sincope ricorrente; ECG basale con sopraslivellamento ST in V1–V3; test provocativo con flecainide positivo per pattern Brugada.”</p>
|
||||
<p class="en"><span class="en-tag">EN</span>“The patient reports recurrent syncope; baseline ECG with ST-segment elevation in V1–V3; flecainide provocation test positive for a Brugada pattern.”</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline" id="pipeline">
|
||||
<div class="stage"><b>Sources</b><span>Cardioref · Genetic data<br>Omics Portal · ECG</span><span class="brain-ai"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage"><b>Staging</b><span>raw replica</span><span class="brain-ai"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage ai"><b>Integration</b><span>3NF · text mining</span><span class="brain-ai"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage"><b>Data Warehouse</b><span>star schema</span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage hot" id="marts"><b>Marts</b><span>dbt · Superset</span></div>
|
||||
</div>
|
||||
<div class="midrow">
|
||||
<div class="concepts">
|
||||
<div class="lbl">four challenges to face · the answers come next</div>
|
||||
<div class="citem"><span class="n">01</span><div><b>Volume without structure</b><span class="d">twenty years of records locked in free-text columns</span></div></div>
|
||||
<div class="citem"><span class="n">02</span><div><b>Ambiguous language</b><span class="d">negation · family history · bilingual shorthand</span></div></div>
|
||||
<div class="citem"><span class="n">03</span><div><b>No shared ontology</b><span class="d">every system names conditions its own way</span></div></div>
|
||||
<div class="citem"><span class="n">04</span><div><b>Proving reliability</b><span class="d">extraction must be demonstrably correct</span></div></div>
|
||||
</div>
|
||||
<div class="railwrap" id="railwrap">
|
||||
<div class="conn-drop" id="conn-drop"></div>
|
||||
<div class="conn-elbow" id="conn-elbow"></div>
|
||||
<div class="rail" id="rail">
|
||||
<div class="lbl">outputs of the datamarts</div>
|
||||
<div class="row"><svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 29 h26"/><rect x="7" y="17" width="5" height="12" rx="1"/><rect x="15" y="9" width="5" height="20" rx="1"/><rect x="23" y="13" width="5" height="16" rx="1"/></svg><b>Dashboards</b><span class="d">clinical review on Superset</span></div>
|
||||
<div class="row"><svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="7" cy="7" r="2.6"/><circle cx="7" cy="27" r="2.6"/><circle cx="17" cy="17" r="3.4"/><circle cx="27" cy="7" r="2.6"/><circle cx="27" cy="27" r="2.6"/><path d="M9.3 8.8 L14.6 14.6"/><path d="M9.3 25.2 L14.6 19.4"/><path d="M19.4 14.6 L24.7 8.8"/><path d="M19.4 19.4 L24.7 25.2"/></svg><b>Machine Learning</b><span class="d">cohort tables for model training</span></div>
|
||||
<div class="row"><svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 27 L11 19 L17 22 L23 13"/><path d="M23 13 L30 6" stroke-dasharray="3 3.4"/><circle cx="30" cy="6" r="2.2" fill="currentColor" stroke="none"/><path d="M3 31 h28" opacity=".35"/></svg><b>Predictive Statistics</b><span class="d">outcomes & risk analysis</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thoth-wrap" id="thothwrap">
|
||||
<div class="t-conn" id="t-conn"></div>
|
||||
<div class="t-head" id="t-head"></div>
|
||||
<div class="thoth-col">
|
||||
<div class="thoth-box"><span class="brain-ai"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span>ThothII — natural-language → SQL over the DWH, builds the final datamarts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">04 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT] Here is one real sentence from a discharge letter — in Italian, as the clinicians wrote it. In this single sentence there is a diagnosis (syncope), an ECG finding (ST elevation in V1–V3), and a drug-challenge result (flecainide positive for Brugada pattern). For a human cardiologist this is readable in two seconds. For a database, this is just a blob of text in a column.
|
||||
The structured tables — demographics, procedures, dates — only tell half the story. The rest is locked inside these free-text fields, in every hospital system we have.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit center-v stats">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Text analysis</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">What the text mining produced</div>
|
||||
<h2>Deterministic AI, audited numbers</h2>
|
||||
<div class="strip">
|
||||
<div class="sstat"><span class="n">58,438</span><span class="l">letters read</span></div>
|
||||
<div class="sstat"><span class="n">73,389</span><span class="l">pathologies extracted</span></div>
|
||||
<div class="sstat"><span class="n">10,908</span><span class="l">tests parsed</span></div>
|
||||
<div class="sstat"><span class="n">2,307</span><span class="l">Brugada patients</span></div>
|
||||
<div class="sstat"><span class="n">≈ 240</span><span class="l">letters / month</span></div>
|
||||
</div>
|
||||
<div class="cycle" id="cycle">
|
||||
<svg class="cycle-svg" viewBox="0 0 1184 270">
|
||||
<defs><marker id="arrw" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#cb333b"/></marker></defs>
|
||||
<g id="cycle-arcs" fill="none" stroke="#cb333b" stroke-width="2">
|
||||
<path marker-end="url(#arrw)"/>
|
||||
<path marker-end="url(#arrw)"/>
|
||||
<path marker-end="url(#arrw)"/>
|
||||
<path marker-end="url(#arrw)"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="vcomp" style="left:592px; top:50px"><svg class="cicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="4" width="20" height="26" rx="2.5"/><path d="M12 11h10"/><path d="M12 16h10"/><path d="M12 21h6"/></svg><b>Letter reader</b><span class="d">every discharge letter, Italian & English</span></div>
|
||||
<div class="vcomp" style="left:992px; top:135px"><svg class="cicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="14.5" cy="14.5" r="8"/><path d="M20.5 20.5 L29 29"/><path d="M11 14.5 h7"/><path d="M14.5 11 v7"/></svg><b>Clinical matcher</b><span class="d">recognises diagnoses, procedures, test outcomes</span></div>
|
||||
<div class="vcomp" style="left:592px; top:220px"><svg class="cicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3.5 L28 8 v8.5 c0 7-4.7 11.6-11 14 C10.7 28.1 6 23.5 6 16.5 V8 Z"/><path d="M12.5 16.5 l3.2 3.2 L23.5 12"/></svg><b>Context guard</b><span class="d">negations & family history kept apart</span></div>
|
||||
<div class="vcomp" style="left:192px; top:135px"><svg class="cicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="17" cy="7" r="3"/><circle cx="7" cy="26.5" r="3"/><circle cx="27" cy="26.5" r="3"/><path d="M17 10 v7"/><path d="M17 17 L8 23.5"/><path d="M17 17 L26 23.5"/></svg><b>Ontology sorter</b><span class="d">every finding into its clinical category</span></div>
|
||||
</div>
|
||||
<div class="note-row">
|
||||
<div class="nitem"><b>Traceable.</b> Every finding carries the version of the rules that produced it (<code>pattern_version</code>): any number can be rebuilt years later.</div>
|
||||
<div class="nitem"><b>Audited.</b> A new rule set goes live only after it proves ≥95% accuracy on 100 records reviewed by hand.</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">05 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT — Sara] I'll take you inside these numbers. We read 58,438 discharge letters — every single one, every night. From them the text miner extracted 73,389 pathology records, classified into our clinical ontology. It parsed 10,908 drug-challenge tests, distinguishing the therapy from the actual test. And at the end of the chain: 2,307 patients with a confirmed Brugada pattern — a cohort nobody could have built by hand.
|
||||
This is not a one-off migration: the pipeline runs every night, and today it processes on average two hundred and forty letters a month. Four simple pieces do the work: a letter reader for the Italian and English text, a clinical matcher that recognises diagnoses, procedures and test outcomes, a context guard that keeps negations and family history apart, and the ontology sorter that files every finding into its clinical category.
|
||||
The key word here is deterministic: no black box. Every number can be traced back to the rule that produced it.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit nlp">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Text analysis</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">Clinical NLP</div>
|
||||
<h2>Reading clinical text is not keyword matching</h2>
|
||||
<div class="hgap"></div>
|
||||
<div class="split">
|
||||
<div class="left">
|
||||
<ul class="points">
|
||||
<li><b>Negation</b> — “fibrillazione atriale <i>esclusa</i>”, “<i>non</i> FA” <span class="g">(“AF ruled out”, “no AF”)</span>: the rule scans a window around every match before deciding</li>
|
||||
<li><b>Family ≠ patient</b> — “padre con FA” <span class="g">(“father with AF”)</span> is attributed to the family member, not to the patient</li>
|
||||
<li><b>Two languages</b> — every pattern matches IT and EN forms: fibrillazione atriale / atrial fibrillation</li>
|
||||
<li><b>Abbreviations & noise</b> — FA, f.a., “TA 140/90” (blood pressure, not a diagnosis)</li>
|
||||
<li><b>One field, many statements</b> — the text is split into clauses before matching</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="code">
|
||||
<div class="code-head"><span>what the rules see</span><span>examples from the corpus</span></div>
|
||||
<pre>“<i>padre con</i> fibrillazione atriale”
|
||||
<span class="en">“father with atrial fibrillation”</span>
|
||||
→ patologia: FA · attribuzione: <b>familiare</b>
|
||||
<span class="en">pathology: AF · attribution: family</span>
|
||||
|
||||
“fibrillazione atriale <i>esclusa</i>”
|
||||
<span class="en">“atrial fibrillation ruled out”</span>
|
||||
→ patologia: FA · <b>negato</b>
|
||||
<span class="en">pathology: AF · negated</span>
|
||||
|
||||
“test provocativo con flecainide
|
||||
positivo per pattern Brugada”
|
||||
<span class="en">“flecainide provocation test, positive for Brugada pattern”</span>
|
||||
→ test: flecainide · esito: <b>POSITIVO</b>
|
||||
<span class="en">test: flecainide · outcome: POSITIVE</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">06 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT — Sara] Why can't we just search for "FA"? Because clinical text lies to naive search. "Fibrillazione atriale esclusa" contains the words of a diagnosis but negates it — so every rule scans a window around the match, looking for negation cues. "Padre con FA" is real atrial fibrillation — but in the father, not the patient: we record it as family history. Letters mix Italian and English, abbreviations collide ("TA" is blood pressure, not a therapy), and one field can contain five different statements — so we split the text into clauses first.
|
||||
Each of these problems has a specific, versioned solution. Sara to expand with real corpus examples.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit center-v">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Text analysis</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">The clinical ontology</div>
|
||||
<h2>Two tiers, one clinical order</h2>
|
||||
<div class="onto-split">
|
||||
<div class="onto-left">
|
||||
<div class="tier-h">TIER 1 · arrhythmic<span class="cnt">11</span></div>
|
||||
<div class="cats">
|
||||
<span class="cat fill"><b>AF</b><span class="it">FA</span></span><span class="cat fill"><b>Brugada</b></span><span class="cat fill"><b>Flutter</b></span><span class="cat fill"><b>AT</b><span class="it">TA</span></span><span class="cat fill"><b>AVNRT</b><span class="it">TRN</span></span><span class="cat fill"><b>PSVT</b><span class="it">WPW / TPSV</span></span><span class="cat fill"><b>VT</b><span class="it">TV</span></span><span class="cat fill"><b>VF</b><span class="it">FV</span></span><span class="cat fill"><b>Ventricular ectopy</b><span class="it">Extrasistolia V.</span></span><span class="cat fill"><b>Syncope</b><span class="it">Sincope</span></span><span class="cat fill"><b>Long QT</b><span class="it">QT lungo</span></span>
|
||||
</div>
|
||||
<div class="tier-h">TIER 2 · structural<span class="cnt">5</span></div>
|
||||
<div class="cats">
|
||||
<span class="cat"><b>AV block</b><span class="it">Blocco AV</span></span><span class="cat"><b>Bundle branch block</b><span class="it">Blocco di branca</span></span><span class="cat"><b>Cardiomyopathy</b><span class="it">Cardiomiopatia</span></span><span class="cat"><b>Heart failure</b><span class="it">Scompenso</span></span><span class="cat"><b>Valvular disease</b><span class="it">Valvulopatia</span></span>
|
||||
</div>
|
||||
<div class="onto-points">
|
||||
<div class="oitem"><span class="n">01</span><div><b>Order encodes clinical precedence</b> <span class="d">— “Brugada” is matched before “TV”, so “substrato per TV” <span class="g">(“substrate for VT”)</span> can't mask a Brugada pattern</span></div></div>
|
||||
<div class="oitem"><span class="n">02</span><div><b>Synonyms live inline</b> <span class="d">— FA / f.a. / fib. atriale / atrial fibrillation → one canonical label</span></div></div>
|
||||
<div class="oitem"><span class="n">03</span><div><b>Versioned like software</b> <span class="d">— semver for the pattern library, <code>pattern_version</code> on every extracted row</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="out-panel">
|
||||
<div class="out-h">The output: text becomes recorded data</div>
|
||||
<div class="out-item"><span class="n">01</span><div><b>Quantitative data on the records</b><span class="d">every procedure and implant the text analysis reads is written back as structured, quantitative values on the record that describes it</span></div></div>
|
||||
<div class="out-item"><span class="n">02</span><div><b>Straight into the DWH flow</b><span class="d">these records enter the data-warehouse generation like any other source</span></div></div>
|
||||
<div class="out-item"><span class="n">03</span><div><b>As if typed at the visit</b><span class="d">the data lands exactly as if clinicians had keyed it in themselves during the visits</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">07 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT — Sara] Extraction needs a target vocabulary — that's the ontology. Tier 1 holds the eleven arrhythmological categories that matter most for our research; Tier 2 holds five structural conditions. The order of the rules is itself clinical knowledge: Brugada patterns are tested before TV, because "substrato per TV" often appears in Brugada reports and would otherwise mask the diagnosis.
|
||||
The ontology is versioned like software: a semantic version for the pattern library, stamped on every extracted row. When we add a synonym or fix a rule, the change is traceable — and the data can be rebuilt.
|
||||
And this is the output of the whole work: reading a letter writes structured, quantitative values back onto the records that describe the procedures and implants it contains — and those records enter the warehouse generation exactly as if the clinicians had typed them during the visits. Text becomes data, indistinguishable from bedside data entry.
|
||||
Sara: review the category list and the Italian labels before final. Labels are now English-first (audience is mostly foreign) — please confirm the EN terms, especially PSVT as the umbrella for WPW/TPSV and AT/AVNRT for TA/TRN.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="arit center-v">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Text analysis</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">Validation</div>
|
||||
<h2>Trusting the text is a process, not a promise</h2>
|
||||
<div class="onto-split">
|
||||
<div class="onto-left">
|
||||
<div class="onto-points">
|
||||
<div class="oitem"><span class="n">01</span><div><b>Quality gates</b> <span class="d">— ≥95% procedure classification, ≥85% pathology detection, validated on 100 manually reviewed records</span></div></div>
|
||||
<div class="oitem"><span class="n">02</span><div><b>Clinical review</b> <span class="d">— the extracted Brugada cohort was validated by clinicians on Superset dashboards</span></div></div>
|
||||
<div class="oitem"><span class="n">03</span><div><b>Feedback loop</b> <span class="d">— ~893 Brugada false positives found and removed in pattern-library v1.3.1</span></div></div>
|
||||
<div class="oitem"><span class="n">04</span><div><b>Nothing is silently dropped</b> <span class="d">— negated and family-attributed findings are stored too, filtered only at the mart layer</span></div></div>
|
||||
</div>
|
||||
<div class="roi roi-quiet"><b class="k">Quality gate</b>An extraction is accepted only after 100 manual reviews confirm the agreed accuracy — the same standard for every new pattern release.</div>
|
||||
</div>
|
||||
<div class="out-panel">
|
||||
<div class="out-h">The advantage: speed, precision, determinism</div>
|
||||
<div class="out-item"><span class="n">01</span><div><b>Machine speed</b><span class="d">twenty years of letters are read by rules, not by hand; when a pattern improves, the whole archive is rebuilt from the letters themselves</span></div></div>
|
||||
<div class="out-item"><span class="n">02</span><div><b>Audited precision</b><span class="d">every release clears the same gate before production: ≥95% procedure classification, ≥85% pathology detection</span></div></div>
|
||||
<div class="out-item"><span class="n">03</span><div><b>Deterministic by design</b><span class="d">the same letter always yields the same values: no LLM sampling, no randomness, every result traceable to a versioned pattern</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">08 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT — Sara] How do we know the extraction is right? Not by faith. Before any pattern release goes to production it passes a gate: on 100 manually reviewed records, procedure classification must reach 95% accuracy, pathology detection 85%. The Brugada cohort itself was reviewed by clinicians on dashboards — and that review fed back into the library: version 1.3.1 alone removed roughly 900 false positives.
|
||||
And we never throw information away: negated and family findings are stored, only filtered when the research mart needs them. Sara: adjust the numbers/wording to what you are comfortable presenting.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 10 · ARITMOLAB TODAY ============ -->
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">AritmoLab today</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">The portal, today</div>
|
||||
<h2>AritmoLab — a quick tour</h2>
|
||||
<div class="shots">
|
||||
<div class="shot">screenshot</div><div class="shot">screenshot</div><div class="shot">screenshot</div>
|
||||
<div class="shot">screenshot</div><div class="shot">screenshot</div><div class="shot">screenshot</div>
|
||||
</div>
|
||||
<p class="hint">[MP: 6-7 real screenshots of the portal, anonymized or demo data]</p>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">09 / 14</span></footer>
|
||||
<aside class="notes">[DRAFT] A quick tour of what AritmoLab looks like today — six screenshots: patient profile, genetics, dashboards, the 360-degree view. What began as four islands is now one platform the Unit uses every day. MP: replace with real screenshots and a spoken tour.</aside>
|
||||
</section>
|
||||
<!-- ============ 11 · CRISIS ============ -->
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">The crisis</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">All good? Not yet.</div>
|
||||
<h2>The warehouse speaks SQL. Research needs more.</h2>
|
||||
<div class="hgap"></div>
|
||||
<ul class="points">
|
||||
<li><b>Clinicians and researchers don't write SQL</b> — a star schema is built for analysts, not for the ward</li>
|
||||
<li><b>Health Intelligence</b> — to present the past, the Unit needs ready-made aggregates: volumes, incidence, outcomes</li>
|
||||
<li><b>Predictive research</b> — multivariate analysis and ML models need clean, wide, cohort-shaped tables</li>
|
||||
<li><b>Hand-made datamarts are the bottleneck</b> — even with coding agents, each one takes hours of careful work</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">The gap</b>Twenty years of data, one warehouse — and no fast road from a research question to an answer.</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">10 / 14</span></footer>
|
||||
<aside class="notes">[DRAFT] And here is the crisis of our story. The warehouse holds everything — but it speaks SQL. A clinician cannot query a star schema between two visits, and a researcher cannot build a study on raw tables. What research needs are datamarts: ready-made views for Health Intelligence, and clean cohort tables for multivariate analysis and machine learning. And building them by hand — even with coding agents helping — takes hours of careful, repetitive work for every single study. This is where our story got stuck.</aside>
|
||||
</section>
|
||||
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">ThothII</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">Role 3 — AI for analysis</div>
|
||||
<h2>Ask the warehouse in plain English</h2>
|
||||
<div class="hgap"></div>
|
||||
<div class="quote">“How many Brugada patients had an effective ablation?”
|
||||
<small>a researcher, in natural language</small>
|
||||
</div>
|
||||
<div class="pipeline">
|
||||
<div class="stage ai"><b>NL → SQL</b><span>AI writes the query over the star schema</span><span class="tag">AI</span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage"><b>Human review</b><span>the researcher checks every proposed query</span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage ai"><b>Datamart</b><span>curated, research-ready marts</span><span class="tag">AI</span></div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="stage"><b>Superset</b><span>statistics & dashboards</span></div>
|
||||
</div>
|
||||
<p class="stat-note">The session is a conversation: refine the question, iterate on the datamart — every step reviewable.</p>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">11 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT] Third role: AI for the analysis itself. This is ThothII, our natural-language layer over the warehouse. A researcher asks "how many Brugada patients had an effective ablation?" in plain English; the AI writes the SQL over the star schema, proposes a datamart, and the researcher reviews the query before it runs. The result lands in Superset as statistics and dashboards.
|
||||
The human stays in the loop — the AI proposes, the researcher approves. This is where the demo will go live in a couple of slides.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 13 · THOTHII TOUR ============ -->
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">A tour of ThothII</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">ThothII, step by step</div>
|
||||
<h2>From question to datamart — the app</h2>
|
||||
<div class="shots">
|
||||
<div class="shot">screenshot</div><div class="shot">screenshot</div><div class="shot">screenshot</div>
|
||||
<div class="shot">screenshot</div><div class="shot">screenshot</div><div class="shot">screenshot</div>
|
||||
</div>
|
||||
<p class="hint">[MP: 6-7 real screenshots of ThothII — ask, review SQL, datamart, dashboard]</p>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">12 / 14</span></footer>
|
||||
<aside class="notes">[DRAFT] Let me walk you through ThothII: the researcher asks the question in plain English; the AI proposes the SQL; the query is reviewed; the datamart is assembled; the dashboard comes alive. Six screens, a few minutes. MP: real screenshots.</aside>
|
||||
</section>
|
||||
<!-- ============ 14 · HAPPY ENDING ============ -->
|
||||
<section class="arit">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">The happy ending</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">From datamart to discovery</div>
|
||||
<h2>One datamart — many questions answered</h2>
|
||||
<div class="charts">
|
||||
<div class="chart"><b>Multivariate analysis</b><span>[SVG chart — next step]</span></div>
|
||||
<div class="chart"><b>Machine learning</b><span>[SVG chart — next step]</span></div>
|
||||
</div>
|
||||
<p class="hint">[Illustrative data, modeled on published arrhythmology predictors — Brugada focus]</p>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">13 / 14</span></footer>
|
||||
<aside class="notes">[DRAFT — next step: two SVG charts, illustrative data modeled on real literature] And this is the happy ending. From one datamart, the Unit can run a multivariate analysis — which factors truly drive arrhythmic risk in Brugada patients — and train machine-learning models on the same table. What used to take weeks of manual data preparation now takes minutes. The AI did not replace the researcher: it gave the researcher back their time.</aside>
|
||||
</section>
|
||||
|
||||
<section class="arit title-slide">
|
||||
<header class="head">
|
||||
<img src="logo.png" alt="">
|
||||
<span class="head-org">AritmoLab · Policlinico San Donato</span>
|
||||
</header>
|
||||
<nav class="band">
|
||||
<span class="band-section">Thank you</span>
|
||||
</nav>
|
||||
<div class="sbody">
|
||||
<div class="kicker">Questions?</div>
|
||||
<h1>Thank you</h1>
|
||||
<div class="hgap"></div>
|
||||
<p class="lead">Want the deep dives? The technical walkthroughs of the text miner, the mapping agents and ThothII are coming as videos on our Substack.</p>
|
||||
<div class="speakers" style="margin-top:32px">
|
||||
<span>Dr. Marco Pancotti - MultiPhysixLab</span>
|
||||
<span>Dr. Sara Paratico - I.R.C.C.S. Policlinico San Donato</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="foot"><span class="g">Role of AI in the Analysis of Unstructured Clinical Databases</span><span class="g">Dr. Marco Pancotti - MultiPhysixLab</span><span class="g">Dr. Sara Paratico - Gruppo San Donato</span><span class="g">San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span class="g num">14 / 14</span></footer>
|
||||
<aside class="notes">
|
||||
[DRAFT] Thank you for your attention. If you want to go deeper — the text miner, the mapping agents, ThothII — we are publishing the technical walkthroughs as videos on our Substack; the link is on the final version of this deck. And now, happy to take your questions.
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</div></div>
|
||||
|
||||
<!-- AI contribution popup -->
|
||||
<div class="ai-pop" id="aiPop">
|
||||
<svg class="ai-line"><line x1="0" y1="0" x2="0" y2="0"></line><circle cx="0" cy="0" r="5"></circle></svg>
|
||||
<div class="ai-pop-card" role="dialog" aria-modal="true">
|
||||
<button class="ai-pop-x" aria-label="Close">✕</button>
|
||||
<div class="ai-pop-kicker" id="aiPopKicker">AI contribution</div>
|
||||
<div class="ai-pop-title"><span class="brain brain-btn" id="aiPopBrain">🧠</span><b id="aiPopTitle"></b></div>
|
||||
<div id="aiPopBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dist/reveal.js"></script>
|
||||
<script src="plugin/notes/notes.js?v=3"></script>
|
||||
<script>
|
||||
Reveal.initialize({
|
||||
width: 1280, height: 720, margin: 0,
|
||||
hash: true, controls: false, progress: false,
|
||||
transition: 'fade', overview: true,
|
||||
plugins: [RevealNotes],
|
||||
});
|
||||
|
||||
// jump dropdown — one select per slide, in the red band
|
||||
const JUMP_TITLES = ['Title', 'Where we started', 'What we wanted to build',
|
||||
'The problem', 'What the text miner reads', 'Hard problems of clinical NLP', 'The clinical ontology',
|
||||
'Trust the text', 'AritmoLab today', 'All good? Not yet', 'ThothII', 'A tour of ThothII',
|
||||
'From datamarts to models', 'Thank you'];
|
||||
document.querySelectorAll('.arit .band').forEach((band) => {
|
||||
const sel = document.createElement('select');
|
||||
sel.className = 'jump';
|
||||
sel.setAttribute('aria-label', 'Jump to slide');
|
||||
sel.innerHTML = JUMP_TITLES.map((t, i) => `<option value="${i}">${String(i + 1).padStart(2, '0')} · ${t}</option>`).join('');
|
||||
sel.addEventListener('change', () => { Reveal.slide(parseInt(sel.value, 10)); sel.blur(); });
|
||||
band.appendChild(sel);
|
||||
});
|
||||
Reveal.on('slidechanged', () => {
|
||||
const idx = Reveal.getIndices().h;
|
||||
document.querySelectorAll('.arit .band select.jump').forEach((s) => { s.selectedIndex = idx; s.blur(); });
|
||||
});
|
||||
|
||||
// slide "The problem": rail drops from Marts center, ThothII arrow rises to Marts.
|
||||
// Reveal scales the canvas — all rect offsets are divided by the current scale.
|
||||
function alignProblem() {
|
||||
const pipe = document.getElementById('pipeline');
|
||||
if (!pipe || !pipe.closest('section').classList.contains('present')) return;
|
||||
const s = Reveal.getScale() || 1;
|
||||
const pr = pipe.getBoundingClientRect();
|
||||
const marts = document.getElementById('marts').getBoundingClientRect();
|
||||
const rail = document.getElementById('rail').getBoundingClientRect();
|
||||
const rwrap = document.getElementById('railwrap').getBoundingClientRect();
|
||||
const drop = document.getElementById('conn-drop');
|
||||
const elbow = document.getElementById('conn-elbow');
|
||||
const cx = (marts.left + marts.width / 2 - rwrap.left) / s;
|
||||
const railLeft = (rail.left - rwrap.left) / s;
|
||||
const railTop = (rail.top - rwrap.top) / s;
|
||||
const mBottom = (marts.bottom - rwrap.top) / s;
|
||||
drop.style.left = (cx - 1) + 'px';
|
||||
drop.style.top = mBottom + 'px';
|
||||
drop.style.height = Math.max(0, railTop - mBottom) + 'px';
|
||||
elbow.style.left = railLeft + 'px';
|
||||
elbow.style.width = Math.max(0, cx - railLeft) + 'px';
|
||||
elbow.style.top = (railTop - 1) + 'px';
|
||||
const twrap = document.getElementById('thothwrap');
|
||||
const box = twrap.querySelector('.thoth-box');
|
||||
const tr = twrap.getBoundingClientRect();
|
||||
const br = box.getBoundingClientRect();
|
||||
const conn = document.getElementById('t-conn');
|
||||
const head = document.getElementById('t-head');
|
||||
const x = tr.width / s - 10;
|
||||
const top = (pr.bottom - tr.top) / s + 2;
|
||||
const boxTop = (br.top - tr.top) / s;
|
||||
conn.style.left = x + 'px';
|
||||
conn.style.top = top + 'px';
|
||||
conn.style.height = Math.max(0, boxTop - top) + 'px';
|
||||
head.style.left = x + 'px';
|
||||
head.style.top = (top - 7) + 'px';
|
||||
}
|
||||
addEventListener('load', alignProblem);
|
||||
addEventListener('resize', alignProblem);
|
||||
Reveal.on('slidechanged', alignProblem);
|
||||
Reveal.on('ready', alignProblem);
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(alignProblem);
|
||||
|
||||
// slide "What the text miner reads": curved arrows between the four components,
|
||||
// drawn from the measured edges of each block so every arrow starts and lands precisely.
|
||||
function alignCycle() {
|
||||
const cyc = document.getElementById('cycle');
|
||||
if (!cyc || !cyc.closest('section').classList.contains('present')) return;
|
||||
const s = Reveal.getScale() || 1;
|
||||
const cr = cyc.getBoundingClientRect();
|
||||
const boxes = [...cyc.querySelectorAll('.vcomp')].map(c => {
|
||||
const r = c.getBoundingClientRect();
|
||||
const l = (r.left - cr.left) / s, t = (r.top - cr.top) / s, w = r.width / s, h = r.height / s;
|
||||
return { l, t, w, h, cx: l + w / 2, cy: t + h / 2 };
|
||||
});
|
||||
function edgePoint(from, to, box) {
|
||||
const dx = to.cx - from.cx, dy = to.cy - from.cy;
|
||||
let t = 1;
|
||||
if (dx > 0) t = Math.min(t, ((box.l + box.w) - from.cx) / dx);
|
||||
if (dx < 0) t = Math.min(t, (box.l - from.cx) / dx);
|
||||
if (dy > 0) t = Math.min(t, ((box.t + box.h) - from.cy) / dy);
|
||||
if (dy < 0) t = Math.min(t, (box.t - from.cy) / dy);
|
||||
return { x: from.cx + dx * t, y: from.cy + dy * t };
|
||||
}
|
||||
const paths = cyc.querySelectorAll('#cycle-arcs path');
|
||||
const seq = [0, 1, 2, 3];
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const a = boxes[seq[k]], b = boxes[seq[(k + 1) % 4]];
|
||||
const start = edgePoint(a, b, a), end = edgePoint(b, a, b);
|
||||
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||
let ox = mid.x - 592, oy = mid.y - 135;
|
||||
const len = Math.hypot(ox, oy) || 1;
|
||||
const cpx = mid.x + (ox / len) * 46, cpy = mid.y + (oy / len) * 46;
|
||||
paths[k].setAttribute('d',
|
||||
`M ${start.x.toFixed(1)} ${start.y.toFixed(1)} Q ${cpx.toFixed(1)} ${cpy.toFixed(1)} ${end.x.toFixed(1)} ${end.y.toFixed(1)}`);
|
||||
}
|
||||
}
|
||||
addEventListener('load', alignCycle);
|
||||
addEventListener('resize', alignCycle);
|
||||
Reveal.on('slidechanged', alignCycle);
|
||||
Reveal.on('ready', alignCycle);
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(alignCycle);
|
||||
|
||||
const AI_CONTRIBS = {
|
||||
ingestion: {
|
||||
title: 'The mappings',
|
||||
body: `
|
||||
<p class="lead">The mappings that turn three raw sources into clean data are <b>crafted by AI</b> — <b>revised by humans</b>.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>AI drafts</b> — LLM agents write the mappings from prompts and schema samples</li>
|
||||
<li><b>What is a mapping?</b> — a plain instruction sheet that tells the system, field by field, where each piece of data comes from and where it has to land</li>
|
||||
<li><b>At scale</b> — <span class="n">19,764</span> lines of instructions, all machine-validated</li>
|
||||
<li><b>Humans revise</b> — every line is checked, then recorded in <b>git</b>, the archive that keeps every version of the instructions and can undo any change</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Positive effects</b>Weeks of hand-writing became days of reviewing.</div>`,
|
||||
},
|
||||
integration: {
|
||||
title: 'Reading the clinical text',
|
||||
body: `
|
||||
<p class="lead">A deterministic, bilingual (IT/EN) pattern library reads the free text inside the nightly pipeline.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Pathologies</b> — <span class="n">73,389</span> extracted from <span class="n">58,438</span> discharge letters, into a two-tier clinical ontology</li>
|
||||
<li><b>Procedures ↔ pathologies</b> — linked at clause level</li>
|
||||
<li><b>Drug-challenge tests</b> — <span class="n">10,908</span> parsed (flecainide, ajmaline, adrenaline, isoprenaline)</li>
|
||||
<li><b>No black box</b> — semver rules, <code>pattern_version</code> on every row, <span class="n">≥95%</span> accuracy gate on 100 manual reviews</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Positive effects</b>58,438 letters of free text became an analysable research asset — automatically, every night.</div>`,
|
||||
},
|
||||
datamarts: {
|
||||
title: 'Datamarts from plain English',
|
||||
body: `
|
||||
<p class="lead">Researchers ask in plain English; the AI writes the SQL over the star schema.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Ask</b> — “how many Brugada patients had an effective ablation?”</li>
|
||||
<li><b>Generate</b> — AI builds the SQL and assembles a curated datamart</li>
|
||||
<li><b>Serve</b> — results flow to Superset for statistics and machine learning</li>
|
||||
<li><b>In the loop</b> — the researcher reviews every proposed query before it runs</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Positive effects</b>A new research datamart in minutes instead of weeks of hand-written SQL — with the researcher approving every query.</div>`,
|
||||
},
|
||||
};
|
||||
const pop = document.getElementById('aiPop');
|
||||
const card = pop.querySelector('.ai-pop-card');
|
||||
const line = pop.querySelector('.ai-line line');
|
||||
const dot = pop.querySelector('.ai-line circle');
|
||||
const closePop = () => pop.classList.remove('open');
|
||||
const placeNear = (brainEl) => {
|
||||
const b = brainEl.getBoundingClientRect();
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
const cw = card.offsetWidth, ch = card.offsetHeight;
|
||||
const bx = b.left + b.width / 2, by = b.top + b.height / 2;
|
||||
const gap = 52;
|
||||
let left;
|
||||
if (bx + gap + cw < vw - 12) left = bx + gap;
|
||||
else if (bx - gap - cw > 12) left = bx - gap - cw;
|
||||
else left = Math.max(12, Math.min(vw - cw - 12, bx - cw / 2));
|
||||
const top = Math.max(12, Math.min(vh - ch - 12, by - ch / 2));
|
||||
card.style.left = Math.round(left) + 'px';
|
||||
card.style.top = Math.round(top) + 'px';
|
||||
const ly = Math.max(top + 24, Math.min(by, top + ch - 24));
|
||||
const ax = left < bx ? left + cw : left;
|
||||
line.setAttribute('x1', bx); line.setAttribute('y1', by);
|
||||
line.setAttribute('x2', ax); line.setAttribute('y2', ly);
|
||||
dot.setAttribute('cx', bx); dot.setAttribute('cy', by);
|
||||
};
|
||||
const MISSING_CONTRIBS = {
|
||||
ci: { kicker: 'The missing piece · 20 years ago', title: 'Clinical Intelligence — the history, made visible',
|
||||
body: `<p class="lead">Twenty years of clinical history, with no way to show it.</p>
|
||||
<ul class="ai-pop-list"><li><b>What it is</b> — dashboards on the Unit's activity and its patients: volumes, incidence, outcomes</li>
|
||||
<li><b>Who needed it</b> — management, audit, clinical research</li>
|
||||
<li><b>Why it never existed</b> — the data was in prose; building dashboards on text was impractical</li></ul>
|
||||
<div class="roi"><b class="k">The gap</b>No fast answer to “how are we doing?” — every question meant a manual query.</div>` },
|
||||
dwh: { kicker: 'The missing piece · 20 years ago', title: 'Datawarehouse — one analysis-ready home',
|
||||
body: `<p class="lead">One home for every record the Unit produces: text, structured fields, genetics, ECG.</p>
|
||||
<ul class="ai-pop-list"><li><b>What it is</b> — a single schema joining all the sources, designed for analysis</li>
|
||||
<li><b>Why it never existed</b> — each system kept its own silo; joining them meant hand-stitching exports</li></ul>
|
||||
<div class="roi"><b class="k">The gap</b>Every research question started with manual exports — and ended in fragile spreadsheets.</div>` },
|
||||
ml: { kicker: 'The missing piece · 20 years ago', title: 'ML-ready data — cohorts for research',
|
||||
body: `<p class="lead">Tables shaped for statistics and model training: clean, wide, reproducible.</p>
|
||||
<ul class="ai-pop-list"><li><b>What it is</b> — one row per patient, features beside outcomes, rebuildable at will</li>
|
||||
<li><b>Why it never existed</b> — building it by hand from free text and separate silos took weeks</li></ul>
|
||||
<div class="roi"><b class="k">The gap</b>Predictive research on the Unit's data stayed out of reach.</div>` },
|
||||
portal: { kicker: 'The missing piece · 20 years ago', title: 'Professional Portal — the 360° view',
|
||||
body: `<p class="lead">The 360° view of the patient: Cardioref and genetics finally in one place.</p>
|
||||
<ul class="ai-pop-list"><li><b>What it is</b> — the grown-up Omics Portal: management, care and research on the same platform</li>
|
||||
<li><b>Why it never existed</b> — the idea was there; the integrated data to feed it was not</li></ul>
|
||||
<div class="roi"><b class="k">The gap</b>A single door for patients and clinicians — promised, never delivered.</div>` },
|
||||
};
|
||||
const START_CONTRIBS = {
|
||||
cardioref: {
|
||||
kicker: 'The starting point · 20 years ago',
|
||||
title: 'Cardioref — the daily workhorse',
|
||||
body: `
|
||||
<p class="lead">For twenty years, the database running the Arrhythmology Unit.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Used for</b> — patient records, the Unit's daily activity, planning and logging of everything that was done</li>
|
||||
<li><b>Good at it</b> — as a management database, it did (and still does) its job</li>
|
||||
<li><b>The catch</b> — most clinical information lives in free text, outside the structured fields</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Problem</b>Almost unusable for research and data extraction: the knowledge is there — locked in prose.</div>`,
|
||||
},
|
||||
genetic: {
|
||||
kicker: 'The starting point · 20 years ago',
|
||||
title: 'Genetic data — an island',
|
||||
body: `
|
||||
<p class="lead">A dedicated system collecting the genetic results of our patients.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Used for</b> — storing genetic tests, variants and lab reports</li>
|
||||
<li><b>The catch</b> — its only output is Excel sheets, with no integration into Cardioref</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Problem</b>Genotypes on one side, clinical records on the other — no way to join them.</div>`,
|
||||
},
|
||||
omics: {
|
||||
kicker: 'The starting point · 20 years ago',
|
||||
title: 'Omics Portal — born too early',
|
||||
body: `
|
||||
<p class="lead">A 360° patient-management system, still in its infancy.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Used for</b> — the first patient-management features beyond the hospital EHR</li>
|
||||
<li><b>The catch</b> — embryonic functionality, and no integration with Cardioref</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Problem</b>A promising idea without a bridge: it could not see the clinical history stored elsewhere.</div>`,
|
||||
},
|
||||
ecg: {
|
||||
kicker: 'The starting point · 20 years ago',
|
||||
title: 'ECG subsystems — one per device',
|
||||
body: `
|
||||
<p class="lead">A constellation of ECG systems, each with its own software and its own silo.</p>
|
||||
<ul class="ai-pop-list">
|
||||
<li><b>Used for</b> — acquiring and storing ECG signals</li>
|
||||
<li><b>The catch</b> — no machine-to-machine communication: the only export is a CSV file, on request</li>
|
||||
</ul>
|
||||
<div class="roi"><b class="k">Problem</b>Every ECG meant a manual download — automation was impossible by design.</div>`,
|
||||
},
|
||||
};
|
||||
const openPop = (key, brainEl, dict) => {
|
||||
const c = (dict || AI_CONTRIBS)[key];
|
||||
if (!c) return;
|
||||
const isAI = (dict || AI_CONTRIBS) === AI_CONTRIBS;
|
||||
document.getElementById('aiPopBrain').style.display = isAI ? 'flex' : 'none';
|
||||
document.getElementById('aiPopKicker').textContent = c.kicker || 'AI contribution';
|
||||
document.getElementById('aiPopTitle').textContent = c.title;
|
||||
document.getElementById('aiPopBody').innerHTML = c.body;
|
||||
pop.classList.add('open');
|
||||
if (brainEl) placeNear(brainEl);
|
||||
};
|
||||
document.addEventListener('click', (e) => {
|
||||
const b = e.target.closest('.brain-btn[data-ai]');
|
||||
if (b) { openPop(b.dataset.ai, b, AI_CONTRIBS); return; }
|
||||
const s = e.target.closest('.isle[data-start]');
|
||||
if (s) { openPop(s.dataset.start, s, START_CONTRIBS); return; }
|
||||
const m = e.target.closest('.todo[data-missing]');
|
||||
if (m) { openPop(m.dataset.missing, m, MISSING_CONTRIBS); return; }
|
||||
if (e.target.closest('.ai-pop-x') || e.target === pop) closePop();
|
||||
});
|
||||
// Escape closes the popup first (capture), without toggling Reveal's overview
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && pop.classList.contains('open')) { closePop(); e.stopPropagation(); }
|
||||
}, true);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
Monokai style - ported by Luigi Maselli - http://grigio.org
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
background: #272822;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-literal,
|
||||
.hljs-strong,
|
||||
.hljs-name {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.hljs-code {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-symbol,
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: #bf79db;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-bullet,
|
||||
.hljs-subst,
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-emphasis,
|
||||
.hljs-type,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-addition,
|
||||
.hljs-variable,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-deletion,
|
||||
.hljs-meta {
|
||||
color: #75715e;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-literal,
|
||||
.hljs-doctag,
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-type,
|
||||
.hljs-selector-id {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
/* highlightjs-line-numbers.js 2.8.0 | (C) 2018 Yauheni Pakala | MIT License | github.com/wcoder/highlightjs-line-numbers.js */
|
||||
!function(r,o){"use strict";var e,i="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",a=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),a=parseInt(r.dataset.lineNumber);if(o==a)return n;var i,l=t.textContent,s=r.textContent;for(a<o&&(i=o,o=a,a=i,i=l,l=s,s=i);0!==n.indexOf(l);)l=l.slice(1);for(;-1===n.lastIndexOf(s);)s=s.slice(0,-1);for(var c=l,u=function(e){for(var n=e;"TABLE"!==n.nodeName;)n=n.parentNode;return n}(t),d=o+1;d<a;++d){var f=p('.{0}[{1}="{2}"]',[h,m,d]);c+="\n"+u.querySelector(f).textContent}return c+="\n"+s}function n(e){try{var n=o.querySelectorAll("code.hljs,code.nohighlight");for(var t in n)n.hasOwnProperty(t)&&(n[t].classList.contains("nohljsln")||d(n[t],e))}catch(e){r.console.error("LineNumbers error: ",e)}}function d(e,n){if("object"==typeof e)e.innerHTML=f(e,n)}function f(e,n){var t,r,o=(t=e,{singleLine:function(e){return!!e.singleLine&&e.singleLine}(r=(r=n)||{}),startFrom:function(e,n){var t=1;isFinite(n.startFrom)&&(t=n.startFrom);var r=function(e,n){return e.hasAttribute(n)?e.getAttribute(n):null}(e,"data-ln-start-from");return null!==r&&(t=function(e,n){if(!e)return n;var t=Number(e);return isFinite(t)?t:n}(r,1)),t}(t,r)});return function e(n){var t=n.childNodes;for(var r in t){var o;t.hasOwnProperty(r)&&(o=t[r],0<(o.textContent.trim().match(a)||[]).length&&(0<o.childNodes.length?e(o):v(o.parentNode)))}}(e),function(e,n){var t=g(e);""===t[t.length-1].trim()&&t.pop();if(1<t.length||n.singleLine){for(var r="",o=0,a=t.length;o<a;o++)r+=p('<tr><td class="{0} {1}" {3}="{5}"><div class="{2}" {3}="{5}"></div></td><td class="{0} {4}" {3}="{5}">{6}</td></tr>',[l,s,c,m,h,o+n.startFrom,0<t[o].length?t[o]:" "]);return p('<table class="{0}">{1}</table>',[i,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r<t.length;r++){o+=p('<span class="{0}">{1}</span>\n',[n,0<t[r].length?t[r]:" "])}e.innerHTML=o.trim()}}function g(e){return 0===e.length?[]:e.split(a)}function p(e,t){return e.replace(/\{(\d+)\}/g,function(e,n){return void 0!==t[n]?t[n]:e})}hljs?(hljs.initLineNumbersOnLoad=function(e){"interactive"===o.readyState||"complete"===o.readyState?n(e):r.addEventListener("DOMContentLoaded",function(){n(e)})},hljs.lineNumbersBlock=d,hljs.lineNumbersValue=function(e,n){if("string"!=typeof e)return;var t=document.createElement("code");return t.innerHTML=e,f(t,n)},(e=o.createElement("style")).type="text/css",e.innerHTML=p(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[i,c,m]),o.getElementsByTagName("head")[0].appendChild(e)):r.console.error("highlight.js not detected!"),document.addEventListener("copy",function(e){var n,t=window.getSelection();!function(e){for(var n=e;n;){if(n.className&&-1!==n.className.indexOf("hljs-ln-code"))return 1;n=n.parentNode}}(t.anchorNode)||(n=-1!==window.navigator.userAgent.indexOf("Edge")?u(t):t.toString(),e.clipboardData.setData("text/plain",n),e.preventDefault())})}(window,document);
|
||||
|
||||
|
||||
/*!
|
||||
* reveal.js plugin that adds syntax highlight support.
|
||||
*/
|
||||
|
||||
const Plugin = {
|
||||
|
||||
id: 'highlight',
|
||||
|
||||
HIGHLIGHT_STEP_DELIMITER: '|',
|
||||
HIGHLIGHT_LINE_DELIMITER: ',',
|
||||
HIGHLIGHT_LINE_RANGE_DELIMITER: '-',
|
||||
|
||||
hljs,
|
||||
|
||||
/**
|
||||
* Highlights code blocks within the given deck.
|
||||
*
|
||||
* Note that this can be called multiple times if
|
||||
* there are multiple presentations on one page.
|
||||
*
|
||||
* @param {Reveal} reveal the reveal.js instance
|
||||
*/
|
||||
init: function( reveal ) {
|
||||
|
||||
// Read the plugin config options and provide fallbacks
|
||||
let config = reveal.getConfig().highlight || {};
|
||||
|
||||
config.highlightOnLoad = typeof config.highlightOnLoad === 'boolean' ? config.highlightOnLoad : true;
|
||||
config.escapeHTML = typeof config.escapeHTML === 'boolean' ? config.escapeHTML : true;
|
||||
|
||||
Array.from( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( block => {
|
||||
|
||||
block.parentNode.classList.add('code-wrapper');
|
||||
|
||||
// Code can optionally be wrapped in script template to avoid
|
||||
// HTML being parsed by the browser (i.e. when you need to
|
||||
// include <, > or & in your code).
|
||||
let substitute = block.querySelector( 'script[type="text/template"]' );
|
||||
if( substitute ) {
|
||||
// textContent handles the HTML entity escapes for us
|
||||
block.textContent = substitute.innerHTML;
|
||||
}
|
||||
|
||||
// Trim whitespace if the "data-trim" attribute is present
|
||||
if( block.hasAttribute( 'data-trim' ) && typeof block.innerHTML.trim === 'function' ) {
|
||||
block.innerHTML = betterTrim( block );
|
||||
}
|
||||
|
||||
// Escape HTML tags unless the "data-noescape" attrbute is present
|
||||
if( config.escapeHTML && !block.hasAttribute( 'data-noescape' )) {
|
||||
block.innerHTML = block.innerHTML.replace( /</g,"<").replace(/>/g, '>' );
|
||||
}
|
||||
|
||||
// Re-highlight when focus is lost (for contenteditable code)
|
||||
block.addEventListener( 'focusout', function( event ) {
|
||||
hljs.highlightElement( event.currentTarget );
|
||||
}, false );
|
||||
|
||||
} );
|
||||
|
||||
// Triggers a callback function before we trigger highlighting
|
||||
if( typeof config.beforeHighlight === 'function' ) {
|
||||
config.beforeHighlight( hljs );
|
||||
}
|
||||
|
||||
// Run initial highlighting for all code
|
||||
if( config.highlightOnLoad ) {
|
||||
Array.from( reveal.getRevealElement().querySelectorAll( 'pre code' ) ).forEach( block => {
|
||||
Plugin.highlightBlock( block );
|
||||
} );
|
||||
}
|
||||
|
||||
// If we're printing to PDF, scroll the code highlights of
|
||||
// all blocks in the deck into view at once
|
||||
reveal.on( 'pdf-ready', function() {
|
||||
[].slice.call( reveal.getRevealElement().querySelectorAll( 'pre code[data-line-numbers].current-fragment' ) ).forEach( function( block ) {
|
||||
Plugin.scrollHighlightedLineIntoView( block, {}, true );
|
||||
} );
|
||||
} );
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlights a code block. If the <code> node has the
|
||||
* 'data-line-numbers' attribute we also generate slide
|
||||
* numbers.
|
||||
*
|
||||
* If the block contains multiple line highlight steps,
|
||||
* we clone the block and create a fragment for each step.
|
||||
*/
|
||||
highlightBlock: function( block ) {
|
||||
|
||||
hljs.highlightElement( block );
|
||||
|
||||
// Don't generate line numbers for empty code blocks
|
||||
if( block.innerHTML.trim().length === 0 ) return;
|
||||
|
||||
if( block.hasAttribute( 'data-line-numbers' ) ) {
|
||||
hljs.lineNumbersBlock( block, { singleLine: true } );
|
||||
|
||||
var scrollState = { currentBlock: block };
|
||||
|
||||
// If there is more than one highlight step, generate
|
||||
// fragments
|
||||
var highlightSteps = Plugin.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) );
|
||||
if( highlightSteps.length > 1 ) {
|
||||
|
||||
// If the original code block has a fragment-index,
|
||||
// each clone should follow in an incremental sequence
|
||||
var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 );
|
||||
|
||||
if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) {
|
||||
fragmentIndex = null;
|
||||
}
|
||||
|
||||
// Generate fragments for all steps except the original block
|
||||
highlightSteps.slice(1).forEach( function( highlight ) {
|
||||
|
||||
var fragmentBlock = block.cloneNode( true );
|
||||
fragmentBlock.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlight ] ) );
|
||||
fragmentBlock.classList.add( 'fragment' );
|
||||
block.parentNode.appendChild( fragmentBlock );
|
||||
Plugin.highlightLines( fragmentBlock );
|
||||
|
||||
if( typeof fragmentIndex === 'number' ) {
|
||||
fragmentBlock.setAttribute( 'data-fragment-index', fragmentIndex );
|
||||
fragmentIndex += 1;
|
||||
}
|
||||
else {
|
||||
fragmentBlock.removeAttribute( 'data-fragment-index' );
|
||||
}
|
||||
|
||||
// Scroll highlights into view as we step through them
|
||||
fragmentBlock.addEventListener( 'visible', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock, scrollState ) );
|
||||
fragmentBlock.addEventListener( 'hidden', Plugin.scrollHighlightedLineIntoView.bind( Plugin, fragmentBlock.previousElementSibling, scrollState ) );
|
||||
|
||||
} );
|
||||
|
||||
block.removeAttribute( 'data-fragment-index' );
|
||||
block.setAttribute( 'data-line-numbers', Plugin.serializeHighlightSteps( [ highlightSteps[0] ] ) );
|
||||
|
||||
}
|
||||
|
||||
// Scroll the first highlight into view when the slide
|
||||
// becomes visible. Note supported in IE11 since it lacks
|
||||
// support for Element.closest.
|
||||
var slide = typeof block.closest === 'function' ? block.closest( 'section:not(.stack)' ) : null;
|
||||
if( slide ) {
|
||||
var scrollFirstHighlightIntoView = function() {
|
||||
Plugin.scrollHighlightedLineIntoView( block, scrollState, true );
|
||||
slide.removeEventListener( 'visible', scrollFirstHighlightIntoView );
|
||||
}
|
||||
slide.addEventListener( 'visible', scrollFirstHighlightIntoView );
|
||||
}
|
||||
|
||||
Plugin.highlightLines( block );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Animates scrolling to the first highlighted line
|
||||
* in the given code block.
|
||||
*/
|
||||
scrollHighlightedLineIntoView: function( block, scrollState, skipAnimation ) {
|
||||
|
||||
cancelAnimationFrame( scrollState.animationFrameID );
|
||||
|
||||
// Match the scroll position of the currently visible
|
||||
// code block
|
||||
if( scrollState.currentBlock ) {
|
||||
block.scrollTop = scrollState.currentBlock.scrollTop;
|
||||
}
|
||||
|
||||
// Remember the current code block so that we can match
|
||||
// its scroll position when showing/hiding fragments
|
||||
scrollState.currentBlock = block;
|
||||
|
||||
var highlightBounds = this.getHighlightedLineBounds( block )
|
||||
var viewportHeight = block.offsetHeight;
|
||||
|
||||
// Subtract padding from the viewport height
|
||||
var blockStyles = getComputedStyle( block );
|
||||
viewportHeight -= parseInt( blockStyles.paddingTop ) + parseInt( blockStyles.paddingBottom );
|
||||
|
||||
// Scroll position which centers all highlights
|
||||
var startTop = block.scrollTop;
|
||||
var targetTop = highlightBounds.top + ( Math.min( highlightBounds.bottom - highlightBounds.top, viewportHeight ) - viewportHeight ) / 2;
|
||||
|
||||
// Account for offsets in position applied to the
|
||||
// <table> that holds our lines of code
|
||||
var lineTable = block.querySelector( '.hljs-ln' );
|
||||
if( lineTable ) targetTop += lineTable.offsetTop - parseInt( blockStyles.paddingTop );
|
||||
|
||||
// Make sure the scroll target is within bounds
|
||||
targetTop = Math.max( Math.min( targetTop, block.scrollHeight - viewportHeight ), 0 );
|
||||
|
||||
if( skipAnimation === true || startTop === targetTop ) {
|
||||
block.scrollTop = targetTop;
|
||||
}
|
||||
else {
|
||||
|
||||
// Don't attempt to scroll if there is no overflow
|
||||
if( block.scrollHeight <= viewportHeight ) return;
|
||||
|
||||
var time = 0;
|
||||
var animate = function() {
|
||||
time = Math.min( time + 0.02, 1 );
|
||||
|
||||
// Update our eased scroll position
|
||||
block.scrollTop = startTop + ( targetTop - startTop ) * Plugin.easeInOutQuart( time );
|
||||
|
||||
// Keep animating unless we've reached the end
|
||||
if( time < 1 ) {
|
||||
scrollState.animationFrameID = requestAnimationFrame( animate );
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* The easing function used when scrolling.
|
||||
*/
|
||||
easeInOutQuart: function( t ) {
|
||||
|
||||
// easeInOutQuart
|
||||
return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t;
|
||||
|
||||
},
|
||||
|
||||
getHighlightedLineBounds: function( block ) {
|
||||
|
||||
var highlightedLines = block.querySelectorAll( '.highlight-line' );
|
||||
if( highlightedLines.length === 0 ) {
|
||||
return { top: 0, bottom: 0 };
|
||||
}
|
||||
else {
|
||||
var firstHighlight = highlightedLines[0];
|
||||
var lastHighlight = highlightedLines[ highlightedLines.length -1 ];
|
||||
|
||||
return {
|
||||
top: firstHighlight.offsetTop,
|
||||
bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Visually emphasize specific lines within a code block.
|
||||
* This only works on blocks with line numbering turned on.
|
||||
*
|
||||
* @param {HTMLElement} block a <code> block
|
||||
* @param {String} [linesToHighlight] The lines that should be
|
||||
* highlighted in this format:
|
||||
* "1" = highlights line 1
|
||||
* "2,5" = highlights lines 2 & 5
|
||||
* "2,5-7" = highlights lines 2, 5, 6 & 7
|
||||
*/
|
||||
highlightLines: function( block, linesToHighlight ) {
|
||||
|
||||
var highlightSteps = Plugin.deserializeHighlightSteps( linesToHighlight || block.getAttribute( 'data-line-numbers' ) );
|
||||
|
||||
if( highlightSteps.length ) {
|
||||
|
||||
highlightSteps[0].forEach( function( highlight ) {
|
||||
|
||||
var elementsToHighlight = [];
|
||||
|
||||
// Highlight a range
|
||||
if( typeof highlight.end === 'number' ) {
|
||||
elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child(n+'+highlight.start+'):nth-child(-n+'+highlight.end+')' ) );
|
||||
}
|
||||
// Highlight a single line
|
||||
else if( typeof highlight.start === 'number' ) {
|
||||
elementsToHighlight = [].slice.call( block.querySelectorAll( 'table tr:nth-child('+highlight.start+')' ) );
|
||||
}
|
||||
|
||||
if( elementsToHighlight.length ) {
|
||||
elementsToHighlight.forEach( function( lineElement ) {
|
||||
lineElement.classList.add( 'highlight-line' );
|
||||
} );
|
||||
|
||||
block.classList.add( 'has-highlights' );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses and formats a user-defined string of line
|
||||
* numbers to highlight.
|
||||
*
|
||||
* @example
|
||||
* Plugin.deserializeHighlightSteps( '1,2|3,5-10' )
|
||||
* // [
|
||||
* // [ { start: 1 }, { start: 2 } ],
|
||||
* // [ { start: 3 }, { start: 5, end: 10 } ]
|
||||
* // ]
|
||||
*/
|
||||
deserializeHighlightSteps: function( highlightSteps ) {
|
||||
|
||||
// Remove whitespace
|
||||
highlightSteps = highlightSteps.replace( /\s/g, '' );
|
||||
|
||||
// Divide up our line number groups
|
||||
highlightSteps = highlightSteps.split( Plugin.HIGHLIGHT_STEP_DELIMITER );
|
||||
|
||||
return highlightSteps.map( function( highlights ) {
|
||||
|
||||
return highlights.split( Plugin.HIGHLIGHT_LINE_DELIMITER ).map( function( highlight ) {
|
||||
|
||||
// Parse valid line numbers
|
||||
if( /^[\d-]+$/.test( highlight ) ) {
|
||||
|
||||
highlight = highlight.split( Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER );
|
||||
|
||||
var lineStart = parseInt( highlight[0], 10 ),
|
||||
lineEnd = parseInt( highlight[1], 10 );
|
||||
|
||||
if( isNaN( lineEnd ) ) {
|
||||
return {
|
||||
start: lineStart
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
start: lineStart,
|
||||
end: lineEnd
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
// If no line numbers are provided, no code will be highlighted
|
||||
else {
|
||||
|
||||
return {};
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes parsed line number data into a string so
|
||||
* that we can store it in the DOM.
|
||||
*/
|
||||
serializeHighlightSteps: function( highlightSteps ) {
|
||||
|
||||
return highlightSteps.map( function( highlights ) {
|
||||
|
||||
return highlights.map( function( highlight ) {
|
||||
|
||||
// Line range
|
||||
if( typeof highlight.end === 'number' ) {
|
||||
return highlight.start + Plugin.HIGHLIGHT_LINE_RANGE_DELIMITER + highlight.end;
|
||||
}
|
||||
// Single line
|
||||
else if( typeof highlight.start === 'number' ) {
|
||||
return highlight.start;
|
||||
}
|
||||
// All lines
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
|
||||
} ).join( Plugin.HIGHLIGHT_LINE_DELIMITER );
|
||||
|
||||
} ).join( Plugin.HIGHLIGHT_STEP_DELIMITER );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Function to perform a better "data-trim" on code snippets
|
||||
// Will slice an indentation amount on each line of the snippet (amount based on the line having the lowest indentation length)
|
||||
function betterTrim(snippetEl) {
|
||||
// Helper functions
|
||||
function trimLeft(val) {
|
||||
// Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill
|
||||
return val.replace(/^[\s\uFEFF\xA0]+/g, '');
|
||||
}
|
||||
function trimLineBreaks(input) {
|
||||
var lines = input.split('\n');
|
||||
|
||||
// Trim line-breaks from the beginning
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '') {
|
||||
lines.splice(i--, 1);
|
||||
} else break;
|
||||
}
|
||||
|
||||
// Trim line-breaks from the end
|
||||
for (var i = lines.length-1; i >= 0; i--) {
|
||||
if (lines[i].trim() === '') {
|
||||
lines.splice(i, 1);
|
||||
} else break;
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Main function for betterTrim()
|
||||
return (function(snippetEl) {
|
||||
var content = trimLineBreaks(snippetEl.innerHTML);
|
||||
var lines = content.split('\n');
|
||||
// Calculate the minimum amount to remove on each line start of the snippet (can be 0)
|
||||
var pad = lines.reduce(function(acc, line) {
|
||||
if (line.length > 0 && trimLeft(line).length > 0 && acc > line.length - trimLeft(line).length) {
|
||||
return line.length - trimLeft(line).length;
|
||||
}
|
||||
return acc;
|
||||
}, Number.POSITIVE_INFINITY);
|
||||
// Slice each line with this amount
|
||||
return lines.map(function(line, index) {
|
||||
return line.slice(pad);
|
||||
})
|
||||
.join('\n');
|
||||
})(snippetEl);
|
||||
}
|
||||
|
||||
export default () => Plugin;
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
|
||||
Zenburn style from voldmar.ru (c) Vladimir Epifanov <voldmar@voldmar.ru>
|
||||
based on dark.css by Ivan Sagalaev
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
background: #3f3f3f;
|
||||
color: #dcdcdc;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-tag {
|
||||
color: #e3ceab;
|
||||
}
|
||||
|
||||
.hljs-template-tag {
|
||||
color: #dcdcdc;
|
||||
}
|
||||
|
||||
.hljs-number {
|
||||
color: #8cd0d3;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute {
|
||||
color: #efdcbc;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #efefaf;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
color: #8f8f8f;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-section,
|
||||
.hljs-type {
|
||||
color: #efef8f;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link {
|
||||
color: #dca3a3;
|
||||
}
|
||||
|
||||
.hljs-deletion,
|
||||
.hljs-string,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: #cc9393;
|
||||
}
|
||||
|
||||
.hljs-addition,
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-meta {
|
||||
color: #7f9f7f;
|
||||
}
|
||||
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,491 +0,0 @@
|
||||
/*!
|
||||
* The reveal.js markdown plugin. Handles parsing of
|
||||
* markdown inside of presentations as well as loading
|
||||
* of external markdown documents.
|
||||
*/
|
||||
|
||||
import { marked } from 'marked';
|
||||
|
||||
const DEFAULT_SLIDE_SEPARATOR = '\r?\n---\r?\n',
|
||||
DEFAULT_VERTICAL_SEPARATOR = null,
|
||||
DEFAULT_NOTES_SEPARATOR = '^\s*notes?:',
|
||||
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
|
||||
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
|
||||
|
||||
const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
|
||||
|
||||
// match an optional line number offset and highlight line numbers
|
||||
// [<line numbers>] or [<offset>: <line numbers>]
|
||||
const CODE_LINE_NUMBER_REGEX = /\[\s*((\d*):)?\s*([\s\d,|-]*)\]/;
|
||||
|
||||
const HTML_ESCAPE_MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
const Plugin = () => {
|
||||
|
||||
// The reveal.js instance this plugin is attached to
|
||||
let deck;
|
||||
|
||||
/**
|
||||
* Retrieves the markdown contents of a slide section
|
||||
* element. Normalizes leading tabs/whitespace.
|
||||
*/
|
||||
function getMarkdownFromSlide( section ) {
|
||||
|
||||
// look for a <script> or <textarea data-template> wrapper
|
||||
const template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
|
||||
|
||||
// strip leading whitespace so it isn't evaluated as code
|
||||
let text = ( template || section ).textContent;
|
||||
|
||||
// restore script end tags
|
||||
text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
|
||||
|
||||
const leadingWs = text.match( /^\n?(\s*)/ )[1].length,
|
||||
leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
|
||||
|
||||
if( leadingTabs > 0 ) {
|
||||
text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}(.*)','g'), function(m, p1) { return '\n' + p1 ; } );
|
||||
}
|
||||
else if( leadingWs > 1 ) {
|
||||
text = text.replace( new RegExp('\\n? {' + leadingWs + '}(.*)', 'g'), function(m, p1) { return '\n' + p1 ; } );
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a markdown slide section element, this will
|
||||
* return all arguments that aren't related to markdown
|
||||
* parsing. Used to forward any other user-defined arguments
|
||||
* to the output markdown slide.
|
||||
*/
|
||||
function getForwardedAttributes( section ) {
|
||||
|
||||
const attributes = section.attributes;
|
||||
const result = [];
|
||||
|
||||
for( let i = 0, len = attributes.length; i < len; i++ ) {
|
||||
const name = attributes[i].name,
|
||||
value = attributes[i].value;
|
||||
|
||||
// disregard attributes that are used for markdown loading/parsing
|
||||
if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
|
||||
|
||||
if( value ) {
|
||||
result.push( name + '="' + value + '"' );
|
||||
}
|
||||
else {
|
||||
result.push( name );
|
||||
}
|
||||
}
|
||||
|
||||
return result.join( ' ' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the given options and fills out default
|
||||
* values for what's not defined.
|
||||
*/
|
||||
function getSlidifyOptions( options ) {
|
||||
const markdownConfig = deck?.getConfig?.().markdown;
|
||||
|
||||
options = options || {};
|
||||
options.separator = options.separator || markdownConfig?.separator || DEFAULT_SLIDE_SEPARATOR;
|
||||
options.verticalSeparator = options.verticalSeparator || markdownConfig?.verticalSeparator || DEFAULT_VERTICAL_SEPARATOR;
|
||||
options.notesSeparator = options.notesSeparator || markdownConfig?.notesSeparator || DEFAULT_NOTES_SEPARATOR;
|
||||
options.attributes = options.attributes || '';
|
||||
|
||||
return options;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for constructing a markdown slide.
|
||||
*/
|
||||
function createMarkdownSlide( content, options ) {
|
||||
|
||||
options = getSlidifyOptions( options );
|
||||
|
||||
const notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
|
||||
|
||||
if( notesMatch.length === 2 ) {
|
||||
content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
|
||||
}
|
||||
|
||||
// prevent script end tags in the content from interfering
|
||||
// with parsing
|
||||
content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
|
||||
|
||||
return '<script type="text/template">' + content + '</script>';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a data string into multiple slides based
|
||||
* on the passed in separator arguments.
|
||||
*/
|
||||
function slidify( markdown, options ) {
|
||||
|
||||
options = getSlidifyOptions( options );
|
||||
|
||||
const separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
|
||||
horizontalSeparatorRegex = new RegExp( options.separator );
|
||||
|
||||
let matches,
|
||||
lastIndex = 0,
|
||||
isHorizontal,
|
||||
wasHorizontal = true,
|
||||
content,
|
||||
sectionStack = [];
|
||||
|
||||
// iterate until all blocks between separators are stacked up
|
||||
while( matches = separatorRegex.exec( markdown ) ) {
|
||||
const notes = null;
|
||||
|
||||
// determine direction (horizontal by default)
|
||||
isHorizontal = horizontalSeparatorRegex.test( matches[0] );
|
||||
|
||||
if( !isHorizontal && wasHorizontal ) {
|
||||
// create vertical stack
|
||||
sectionStack.push( [] );
|
||||
}
|
||||
|
||||
// pluck slide content from markdown input
|
||||
content = markdown.substring( lastIndex, matches.index );
|
||||
|
||||
if( isHorizontal && wasHorizontal ) {
|
||||
// add to horizontal stack
|
||||
sectionStack.push( content );
|
||||
}
|
||||
else {
|
||||
// add to vertical stack
|
||||
sectionStack[sectionStack.length-1].push( content );
|
||||
}
|
||||
|
||||
lastIndex = separatorRegex.lastIndex;
|
||||
wasHorizontal = isHorizontal;
|
||||
}
|
||||
|
||||
// add the remaining slide
|
||||
( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
|
||||
|
||||
let markdownSections = '';
|
||||
|
||||
// flatten the hierarchical stack, and insert <section data-markdown> tags
|
||||
for( let i = 0, len = sectionStack.length; i < len; i++ ) {
|
||||
// vertical
|
||||
if( sectionStack[i] instanceof Array ) {
|
||||
markdownSections += '<section '+ options.attributes +'>';
|
||||
|
||||
sectionStack[i].forEach( function( child ) {
|
||||
markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
|
||||
} );
|
||||
|
||||
markdownSections += '</section>';
|
||||
}
|
||||
else {
|
||||
markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
|
||||
}
|
||||
}
|
||||
|
||||
return markdownSections;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses any current data-markdown slides, splits
|
||||
* multi-slide markdown into separate sections and
|
||||
* handles loading of external markdown.
|
||||
*/
|
||||
function processSlides( scope ) {
|
||||
|
||||
return new Promise( function( resolve ) {
|
||||
|
||||
const externalPromises = [];
|
||||
|
||||
[].slice.call( scope.querySelectorAll( 'section[data-markdown]:not([data-markdown-parsed])') ).forEach( function( section, i ) {
|
||||
|
||||
if( section.getAttribute( 'data-markdown' ).length ) {
|
||||
|
||||
externalPromises.push( loadExternalMarkdown( section ).then(
|
||||
|
||||
// Finished loading external file
|
||||
function( xhr, url ) {
|
||||
section.outerHTML = slidify( xhr.responseText, {
|
||||
separator: section.getAttribute( 'data-separator' ),
|
||||
verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
|
||||
notesSeparator: section.getAttribute( 'data-separator-notes' ),
|
||||
attributes: getForwardedAttributes( section )
|
||||
});
|
||||
},
|
||||
|
||||
// Failed to load markdown
|
||||
function( xhr, url ) {
|
||||
section.outerHTML = '<section data-state="alert">' +
|
||||
'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
|
||||
'Check your browser\'s JavaScript console for more details.' +
|
||||
'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
|
||||
'</section>';
|
||||
}
|
||||
|
||||
) );
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
section.outerHTML = slidify( getMarkdownFromSlide( section ), {
|
||||
separator: section.getAttribute( 'data-separator' ),
|
||||
verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
|
||||
notesSeparator: section.getAttribute( 'data-separator-notes' ),
|
||||
attributes: getForwardedAttributes( section )
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Promise.all( externalPromises ).then( resolve );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
function loadExternalMarkdown( section ) {
|
||||
|
||||
return new Promise( function( resolve, reject ) {
|
||||
|
||||
const xhr = new XMLHttpRequest(),
|
||||
url = section.getAttribute( 'data-markdown' );
|
||||
|
||||
const datacharset = section.getAttribute( 'data-charset' );
|
||||
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
|
||||
if( datacharset !== null && datacharset !== '' ) {
|
||||
xhr.overrideMimeType( 'text/html; charset=' + datacharset );
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function( section, xhr ) {
|
||||
if( xhr.readyState === 4 ) {
|
||||
// file protocol yields status code 0 (useful for local debug, mobile applications etc.)
|
||||
if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
|
||||
|
||||
resolve( xhr, url );
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
reject( xhr, url );
|
||||
|
||||
}
|
||||
}
|
||||
}.bind( this, section, xhr );
|
||||
|
||||
xhr.open( 'GET', url, true );
|
||||
|
||||
try {
|
||||
xhr.send();
|
||||
}
|
||||
catch ( e ) {
|
||||
console.warn( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
|
||||
resolve( xhr, url );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node value has the attributes pattern.
|
||||
* If yes, extract it and add that value as one or several attributes
|
||||
* to the target element.
|
||||
*
|
||||
* You need Cache Killer on Chrome to see the effect on any FOM transformation
|
||||
* directly on refresh (F5)
|
||||
* http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
|
||||
*/
|
||||
function addAttributeInElement( node, elementTarget, separator ) {
|
||||
|
||||
const markdownClassesInElementsRegex = new RegExp( separator, 'mg' );
|
||||
const markdownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"]+?)\"|(data-[^\"= ]+?)(?=[\" ])", 'mg' );
|
||||
let nodeValue = node.nodeValue;
|
||||
let matches,
|
||||
matchesClass;
|
||||
if( matches = markdownClassesInElementsRegex.exec( nodeValue ) ) {
|
||||
|
||||
const classes = matches[1];
|
||||
nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( markdownClassesInElementsRegex.lastIndex );
|
||||
node.nodeValue = nodeValue;
|
||||
while( matchesClass = markdownClassRegex.exec( classes ) ) {
|
||||
if( matchesClass[2] ) {
|
||||
elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
|
||||
} else {
|
||||
elementTarget.setAttribute( matchesClass[3], "" );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add attributes to the parent element of a text node,
|
||||
* or the element of an attribute node.
|
||||
*/
|
||||
function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
|
||||
|
||||
if ( element !== null && element.childNodes !== undefined && element.childNodes.length > 0 ) {
|
||||
let previousParentElement = element;
|
||||
for( let i = 0; i < element.childNodes.length; i++ ) {
|
||||
const childElement = element.childNodes[i];
|
||||
if ( i > 0 ) {
|
||||
let j = i - 1;
|
||||
while ( j >= 0 ) {
|
||||
const aPreviousChildElement = element.childNodes[j];
|
||||
if ( typeof aPreviousChildElement.setAttribute === 'function' && aPreviousChildElement.tagName !== "BR" ) {
|
||||
previousParentElement = aPreviousChildElement;
|
||||
break;
|
||||
}
|
||||
j = j - 1;
|
||||
}
|
||||
}
|
||||
let parentSection = section;
|
||||
if( childElement.nodeName === "section" ) {
|
||||
parentSection = childElement ;
|
||||
previousParentElement = childElement ;
|
||||
}
|
||||
if ( typeof childElement.setAttribute === 'function' || childElement.nodeType === Node.COMMENT_NODE ) {
|
||||
addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( element.nodeType === Node.COMMENT_NODE ) {
|
||||
if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) === false ) {
|
||||
addAttributeInElement( element, section, separatorSectionAttributes );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any current data-markdown slides in the
|
||||
* DOM to HTML.
|
||||
*/
|
||||
function convertSlides() {
|
||||
|
||||
const sections = deck.getRevealElement().querySelectorAll( '[data-markdown]:not([data-markdown-parsed])');
|
||||
|
||||
[].slice.call( sections ).forEach( function( section ) {
|
||||
|
||||
section.setAttribute( 'data-markdown-parsed', true )
|
||||
|
||||
const notes = section.querySelector( 'aside.notes' );
|
||||
const markdown = getMarkdownFromSlide( section );
|
||||
|
||||
section.innerHTML = marked( markdown );
|
||||
addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) ||
|
||||
section.parentNode.getAttribute( 'data-element-attributes' ) ||
|
||||
DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
|
||||
section.getAttribute( 'data-attributes' ) ||
|
||||
section.parentNode.getAttribute( 'data-attributes' ) ||
|
||||
DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
|
||||
|
||||
// If there were notes, we need to re-add them after
|
||||
// having overwritten the section's HTML
|
||||
if( notes ) {
|
||||
section.appendChild( notes );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
}
|
||||
|
||||
function escapeForHTML( input ) {
|
||||
|
||||
return input.replace( /([&<>'"])/g, char => HTML_ESCAPE_MAP[char] );
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'markdown',
|
||||
|
||||
/**
|
||||
* Starts processing and converting Markdown within the
|
||||
* current reveal.js deck.
|
||||
*/
|
||||
init: function( reveal ) {
|
||||
|
||||
deck = reveal;
|
||||
|
||||
let { renderer, animateLists, ...markedOptions } = deck.getConfig().markdown || {};
|
||||
|
||||
if( !renderer ) {
|
||||
renderer = new marked.Renderer();
|
||||
|
||||
renderer.code = ( code, language ) => {
|
||||
|
||||
// Off by default
|
||||
let lineNumberOffset = '';
|
||||
let lineNumbers = '';
|
||||
|
||||
// Users can opt in to show line numbers and highlight
|
||||
// specific lines.
|
||||
// ```javascript [] show line numbers
|
||||
// ```javascript [1,4-8] highlights lines 1 and 4-8
|
||||
// optional line number offset:
|
||||
// ```javascript [25: 1,4-8] start line numbering at 25,
|
||||
// highlights lines 1 (numbered as 25) and 4-8 (numbered as 28-32)
|
||||
if( CODE_LINE_NUMBER_REGEX.test( language ) ) {
|
||||
let lineNumberOffsetMatch = language.match( CODE_LINE_NUMBER_REGEX )[2];
|
||||
if (lineNumberOffsetMatch){
|
||||
lineNumberOffset = `data-ln-start-from="${lineNumberOffsetMatch.trim()}"`;
|
||||
}
|
||||
|
||||
lineNumbers = language.match( CODE_LINE_NUMBER_REGEX )[3].trim();
|
||||
lineNumbers = `data-line-numbers="${lineNumbers}"`;
|
||||
language = language.replace( CODE_LINE_NUMBER_REGEX, '' ).trim();
|
||||
}
|
||||
|
||||
// Escape before this gets injected into the DOM to
|
||||
// avoid having the HTML parser alter our code before
|
||||
// highlight.js is able to read it
|
||||
code = escapeForHTML( code );
|
||||
|
||||
// return `<pre><code ${lineNumbers} class="${language}">${code}</code></pre>`;
|
||||
|
||||
return `<pre><code ${lineNumbers} ${lineNumberOffset} class="${language}">${code}</code></pre>`;
|
||||
};
|
||||
}
|
||||
|
||||
if( animateLists === true ) {
|
||||
renderer.listitem = text => `<li class="fragment">${text}</li>`;
|
||||
}
|
||||
|
||||
marked.setOptions( {
|
||||
renderer,
|
||||
...markedOptions
|
||||
} );
|
||||
|
||||
return processSlides( deck.getRevealElement() ).then( convertSlides );
|
||||
|
||||
},
|
||||
|
||||
// TODO: Do these belong in the API?
|
||||
processSlides: processSlides,
|
||||
convertSlides: convertSlides,
|
||||
slidify: slidify,
|
||||
marked: marked
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* A plugin which enables rendering of math equations inside
|
||||
* of reveal.js slides. Essentially a thin wrapper for KaTeX.
|
||||
*
|
||||
* @author Hakim El Hattab
|
||||
* @author Gerhard Burger
|
||||
*/
|
||||
export const KaTeX = () => {
|
||||
let deck;
|
||||
|
||||
let defaultOptions = {
|
||||
version: 'latest',
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true}, // Note: $$ has to come before $
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\(', right: '\\)', display: false},
|
||||
{left: '\\[', right: '\\]', display: true}
|
||||
],
|
||||
ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre']
|
||||
}
|
||||
|
||||
const loadCss = src => {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = src;
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a JavaScript file and returns a Promise for when it is loaded
|
||||
* Credits: https://aaronsmith.online/easily-load-an-external-script-using-javascript/
|
||||
*/
|
||||
const loadScript = src => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
script.src = src
|
||||
document.head.append(script)
|
||||
})
|
||||
};
|
||||
|
||||
async function loadScripts(urls) {
|
||||
for(const url of urls) {
|
||||
await loadScript(url);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'katex',
|
||||
|
||||
init: function (reveal) {
|
||||
|
||||
deck = reveal;
|
||||
|
||||
let revealOptions = deck.getConfig().katex || {};
|
||||
|
||||
let options = {...defaultOptions, ...revealOptions};
|
||||
const {local, version, extensions, ...katexOptions} = options;
|
||||
|
||||
let baseUrl = options.local || 'https://cdn.jsdelivr.net/npm/katex';
|
||||
let versionString = options.local ? '' : '@' + options.version;
|
||||
|
||||
let cssUrl = baseUrl + versionString + '/dist/katex.min.css';
|
||||
let katexUrl = baseUrl + versionString + '/dist/katex.min.js';
|
||||
let mhchemUrl = baseUrl + versionString + '/dist/contrib/mhchem.min.js'
|
||||
let karUrl = baseUrl + versionString + '/dist/contrib/auto-render.min.js';
|
||||
|
||||
let katexScripts = [katexUrl];
|
||||
if(options.extensions && options.extensions.includes("mhchem")) {
|
||||
katexScripts.push(mhchemUrl);
|
||||
}
|
||||
katexScripts.push(karUrl);
|
||||
|
||||
const renderMath = () => {
|
||||
renderMathInElement(reveal.getSlidesElement(), katexOptions);
|
||||
deck.layout();
|
||||
}
|
||||
|
||||
loadCss(cssUrl);
|
||||
|
||||
// For some reason dynamically loading with defer attribute doesn't result in the expected behavior, the below code does
|
||||
loadScripts(katexScripts).then(() => {
|
||||
if( deck.isReady() ) {
|
||||
renderMath();
|
||||
}
|
||||
else {
|
||||
deck.on( 'ready', renderMath.bind( this ) );
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
const t=()=>{let t,e={messageStyle:"none",tex2jax:{inlineMath:[["$","$"],["\\(","\\)"]],skipTags:["script","noscript","style","textarea","pre"]},skipStartupTypeset:!0};return{id:"mathjax2",init:function(a){t=a;let n=t.getConfig().mathjax2||t.getConfig().math||{},i={...e,...n},s=(i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js")+"?config="+(i.config||"TeX-AMS_HTML-full");i.tex2jax={...e.tex2jax,...n.tex2jax},i.mathjax=i.config=null,function(t,e){let a=document.querySelector("head"),n=document.createElement("script");n.type="text/javascript",n.src=t;let i=()=>{"function"==typeof e&&(e.call(),e=null)};n.onload=i,n.onreadystatechange=()=>{"loaded"===this.readyState&&i()},a.appendChild(n)}(s,(function(){MathJax.Hub.Config(i),MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.getRevealElement()]),MathJax.Hub.Queue(t.layout),t.on("slidechanged",(function(t){MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.currentSlide])}))}))}}},e=t;
|
||||
/*!
|
||||
* This plugin is a wrapper for the MathJax2,
|
||||
* MathJax3 and KaTeX typesetter plugins.
|
||||
*/
|
||||
var a=Plugin=Object.assign(e(),{KaTeX:()=>{let t,e={version:"latest",delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],ignoredTags:["script","noscript","style","textarea","pre"]};const a=t=>new Promise(((e,a)=>{const n=document.createElement("script");n.type="text/javascript",n.onload=e,n.onerror=a,n.src=t,document.head.append(n)}));return{id:"katex",init:function(n){t=n;let i=t.getConfig().katex||{},s={...e,...i};const{local:l,version:o,extensions:r,...c}=s;let d=s.local||"https://cdn.jsdelivr.net/npm/katex",p=s.local?"":"@"+s.version,u=d+p+"/dist/katex.min.css",h=d+p+"/dist/contrib/mhchem.min.js",x=d+p+"/dist/contrib/auto-render.min.js",m=[d+p+"/dist/katex.min.js"];s.extensions&&s.extensions.includes("mhchem")&&m.push(h),m.push(x);const f=()=>{renderMathInElement(n.getSlidesElement(),c),t.layout()};(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)})(u),async function(t){for(const e of t)await a(e)}(m).then((()=>{t.isReady()?f():t.on("ready",f.bind(this))}))}}},MathJax2:t,MathJax3:()=>{let t,e={tex:{inlineMath:[["$","$"],["\\(","\\)"]]},options:{skipHtmlTags:["script","noscript","style","textarea","pre"]},startup:{ready:()=>{MathJax.startup.defaultReady(),MathJax.startup.promise.then((()=>{t.layout()}))}}};return{id:"mathjax3",init:function(a){t=a;let n=t.getConfig().mathjax3||{},i={...e,...n};i.tex={...e.tex,...n.tex},i.options={...e.options,...n.options},i.startup={...e.startup,...n.startup};let s=i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";i.mathjax=null,window.MathJax=i,function(t,e){let a=document.createElement("script");a.type="text/javascript",a.id="MathJax-script",a.src=t,a.async=!0,a.onload=()=>{"function"==typeof e&&(e.call(),e=null)},document.head.appendChild(a)}(s,(function(){t.addEventListener("slidechanged",(function(t){MathJax.typeset()}))}))}}}});export{a as default};
|
||||
@@ -1 +0,0 @@
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).RevealMath=e()}(this,(function(){"use strict";const t=()=>{let t,e={messageStyle:"none",tex2jax:{inlineMath:[["$","$"],["\\(","\\)"]],skipTags:["script","noscript","style","textarea","pre"]},skipStartupTypeset:!0};return{id:"mathjax2",init:function(n){t=n;let a=t.getConfig().mathjax2||t.getConfig().math||{},i={...e,...a},s=(i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js")+"?config="+(i.config||"TeX-AMS_HTML-full");i.tex2jax={...e.tex2jax,...a.tex2jax},i.mathjax=i.config=null,function(t,e){let n=document.querySelector("head"),a=document.createElement("script");a.type="text/javascript",a.src=t;let i=()=>{"function"==typeof e&&(e.call(),e=null)};a.onload=i,a.onreadystatechange=()=>{"loaded"===this.readyState&&i()},n.appendChild(a)}(s,(function(){MathJax.Hub.Config(i),MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.getRevealElement()]),MathJax.Hub.Queue(t.layout),t.on("slidechanged",(function(t){MathJax.Hub.Queue(["Typeset",MathJax.Hub,t.currentSlide])}))}))}}},e=t;return Plugin=Object.assign(e(),{KaTeX:()=>{let t,e={version:"latest",delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],ignoredTags:["script","noscript","style","textarea","pre"]};const n=t=>new Promise(((e,n)=>{const a=document.createElement("script");a.type="text/javascript",a.onload=e,a.onerror=n,a.src=t,document.head.append(a)}));return{id:"katex",init:function(a){t=a;let i=t.getConfig().katex||{},s={...e,...i};const{local:o,version:l,extensions:r,...c}=s;let d=s.local||"https://cdn.jsdelivr.net/npm/katex",u=s.local?"":"@"+s.version,p=d+u+"/dist/katex.min.css",h=d+u+"/dist/contrib/mhchem.min.js",x=d+u+"/dist/contrib/auto-render.min.js",m=[d+u+"/dist/katex.min.js"];s.extensions&&s.extensions.includes("mhchem")&&m.push(h),m.push(x);const f=()=>{renderMathInElement(a.getSlidesElement(),c),t.layout()};(t=>{let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)})(p),async function(t){for(const e of t)await n(e)}(m).then((()=>{t.isReady()?f():t.on("ready",f.bind(this))}))}}},MathJax2:t,MathJax3:()=>{let t,e={tex:{inlineMath:[["$","$"],["\\(","\\)"]]},options:{skipHtmlTags:["script","noscript","style","textarea","pre"]},startup:{ready:()=>{MathJax.startup.defaultReady(),MathJax.startup.promise.then((()=>{t.layout()}))}}};return{id:"mathjax3",init:function(n){t=n;let a=t.getConfig().mathjax3||{},i={...e,...a};i.tex={...e.tex,...a.tex},i.options={...e.options,...a.options},i.startup={...e.startup,...a.startup};let s=i.mathjax||"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";i.mathjax=null,window.MathJax=i,function(t,e){let n=document.createElement("script");n.type="text/javascript",n.id="MathJax-script",n.src=t,n.async=!0,n.onload=()=>{"function"==typeof e&&(e.call(),e=null)},document.head.appendChild(n)}(s,(function(){t.addEventListener("slidechanged",(function(t){MathJax.typeset()}))}))}}}})}));
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* A plugin which enables rendering of math equations inside
|
||||
* of reveal.js slides. Essentially a thin wrapper for MathJax.
|
||||
*
|
||||
* @author Hakim El Hattab
|
||||
*/
|
||||
export const MathJax2 = () => {
|
||||
|
||||
// The reveal.js instance this plugin is attached to
|
||||
let deck;
|
||||
|
||||
let defaultOptions = {
|
||||
messageStyle: 'none',
|
||||
tex2jax: {
|
||||
inlineMath: [ [ '$', '$' ], [ '\\(', '\\)' ] ],
|
||||
skipTags: [ 'script', 'noscript', 'style', 'textarea', 'pre' ]
|
||||
},
|
||||
skipStartupTypeset: true
|
||||
};
|
||||
|
||||
function loadScript( url, callback ) {
|
||||
|
||||
let head = document.querySelector( 'head' );
|
||||
let script = document.createElement( 'script' );
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
|
||||
// Wrapper for callback to make sure it only fires once
|
||||
let finish = () => {
|
||||
if( typeof callback === 'function' ) {
|
||||
callback.call();
|
||||
callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
script.onload = finish;
|
||||
|
||||
// IE
|
||||
script.onreadystatechange = () => {
|
||||
if ( this.readyState === 'loaded' ) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
// Normal browsers
|
||||
head.appendChild( script );
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'mathjax2',
|
||||
|
||||
init: function( reveal ) {
|
||||
|
||||
deck = reveal;
|
||||
|
||||
let revealOptions = deck.getConfig().mathjax2 || deck.getConfig().math || {};
|
||||
|
||||
let options = { ...defaultOptions, ...revealOptions };
|
||||
let mathjax = options.mathjax || 'https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js';
|
||||
let config = options.config || 'TeX-AMS_HTML-full';
|
||||
let url = mathjax + '?config=' + config;
|
||||
|
||||
options.tex2jax = { ...defaultOptions.tex2jax, ...revealOptions.tex2jax };
|
||||
|
||||
options.mathjax = options.config = null;
|
||||
|
||||
loadScript( url, function() {
|
||||
|
||||
MathJax.Hub.Config( options );
|
||||
|
||||
// Typeset followed by an immediate reveal.js layout since
|
||||
// the typesetting process could affect slide height
|
||||
MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, deck.getRevealElement() ] );
|
||||
MathJax.Hub.Queue( deck.layout );
|
||||
|
||||
// Reprocess equations in slides when they turn visible
|
||||
deck.on( 'slidechanged', function( event ) {
|
||||
|
||||
MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, event.currentSlide ] );
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* A plugin which enables rendering of math equations inside
|
||||
* of reveal.js slides. Essentially a thin wrapper for MathJax 3
|
||||
*
|
||||
* @author Hakim El Hattab
|
||||
* @author Gerhard Burger
|
||||
*/
|
||||
export const MathJax3 = () => {
|
||||
|
||||
// The reveal.js instance this plugin is attached to
|
||||
let deck;
|
||||
|
||||
let defaultOptions = {
|
||||
tex: {
|
||||
inlineMath: [ [ '$', '$' ], [ '\\(', '\\)' ] ]
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: [ 'script', 'noscript', 'style', 'textarea', 'pre' ]
|
||||
},
|
||||
startup: {
|
||||
ready: () => {
|
||||
MathJax.startup.defaultReady();
|
||||
MathJax.startup.promise.then(() => {
|
||||
deck.layout();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function loadScript( url, callback ) {
|
||||
|
||||
let script = document.createElement( 'script' );
|
||||
script.type = "text/javascript"
|
||||
script.id = "MathJax-script"
|
||||
script.src = url;
|
||||
script.async = true
|
||||
|
||||
// Wrapper for callback to make sure it only fires once
|
||||
script.onload = () => {
|
||||
if (typeof callback === 'function') {
|
||||
callback.call();
|
||||
callback = null;
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild( script );
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'mathjax3',
|
||||
init: function(reveal) {
|
||||
|
||||
deck = reveal;
|
||||
|
||||
let revealOptions = deck.getConfig().mathjax3 || {};
|
||||
let options = {...defaultOptions, ...revealOptions};
|
||||
options.tex = {...defaultOptions.tex, ...revealOptions.tex}
|
||||
options.options = {...defaultOptions.options, ...revealOptions.options}
|
||||
options.startup = {...defaultOptions.startup, ...revealOptions.startup}
|
||||
|
||||
let url = options.mathjax || 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
|
||||
options.mathjax = null;
|
||||
|
||||
window.MathJax = options;
|
||||
|
||||
loadScript( url, function() {
|
||||
// Reprocess equations in slides when they turn visible
|
||||
deck.addEventListener( 'slidechanged', function( event ) {
|
||||
MathJax.typeset();
|
||||
} );
|
||||
} );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import {KaTeX} from "./katex";
|
||||
import {MathJax2} from "./mathjax2";
|
||||
import {MathJax3} from "./mathjax3";
|
||||
|
||||
const defaultTypesetter = MathJax2;
|
||||
|
||||
/*!
|
||||
* This plugin is a wrapper for the MathJax2,
|
||||
* MathJax3 and KaTeX typesetter plugins.
|
||||
*/
|
||||
export default Plugin = Object.assign( defaultTypesetter(), {
|
||||
KaTeX,
|
||||
MathJax2,
|
||||
MathJax3
|
||||
} );
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,267 +0,0 @@
|
||||
import speakerViewHTML from './speaker-view.html'
|
||||
|
||||
import { marked } from 'marked';
|
||||
|
||||
/**
|
||||
* Handles opening of and synchronization with the reveal.js
|
||||
* notes window.
|
||||
*
|
||||
* Handshake process:
|
||||
* 1. This window posts 'connect' to notes window
|
||||
* - Includes URL of presentation to show
|
||||
* 2. Notes window responds with 'connected' when it is available
|
||||
* 3. This window proceeds to send the current presentation state
|
||||
* to the notes window
|
||||
*/
|
||||
const Plugin = () => {
|
||||
|
||||
let connectInterval;
|
||||
let speakerWindow = null;
|
||||
let deck;
|
||||
|
||||
/**
|
||||
* Opens a new speaker view window.
|
||||
*/
|
||||
function openSpeakerWindow() {
|
||||
|
||||
// If a window is already open, focus it
|
||||
if( speakerWindow && !speakerWindow.closed ) {
|
||||
speakerWindow.focus();
|
||||
}
|
||||
else {
|
||||
speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
|
||||
speakerWindow.marked = marked;
|
||||
speakerWindow.document.write( speakerViewHTML );
|
||||
|
||||
if( !speakerWindow ) {
|
||||
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
|
||||
return;
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect with an existing speaker view window.
|
||||
*/
|
||||
function reconnectSpeakerWindow( reconnectWindow ) {
|
||||
|
||||
if( speakerWindow && !speakerWindow.closed ) {
|
||||
speakerWindow.focus();
|
||||
}
|
||||
else {
|
||||
speakerWindow = reconnectWindow;
|
||||
window.addEventListener( 'message', onPostMessage );
|
||||
onConnected();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the notes window through a postmessage handshake.
|
||||
* Using postmessage enables us to work in situations where the
|
||||
* origins differ, such as a presentation being opened from the
|
||||
* file system.
|
||||
*/
|
||||
function connect() {
|
||||
|
||||
const presentationURL = deck.getConfig().url;
|
||||
|
||||
const url = typeof presentationURL === 'string' ? presentationURL :
|
||||
window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;
|
||||
|
||||
// Keep trying to connect until we get a 'connected' message back
|
||||
connectInterval = setInterval( function() {
|
||||
speakerWindow.postMessage( JSON.stringify( {
|
||||
namespace: 'reveal-notes',
|
||||
type: 'connect',
|
||||
state: deck.getState(),
|
||||
url
|
||||
} ), '*' );
|
||||
}, 500 );
|
||||
|
||||
window.addEventListener( 'message', onPostMessage );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified Reveal.js method with the provided argument
|
||||
* and then pushes the result to the notes frame.
|
||||
*/
|
||||
function callRevealApi( methodName, methodArguments, callId ) {
|
||||
|
||||
let result = deck[methodName].apply( deck, methodArguments );
|
||||
speakerWindow.postMessage( JSON.stringify( {
|
||||
namespace: 'reveal-notes',
|
||||
type: 'return',
|
||||
result,
|
||||
callId
|
||||
} ), '*' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the current slide data to the notes window.
|
||||
*/
|
||||
function post( event ) {
|
||||
|
||||
let slideElement = deck.getCurrentSlide(),
|
||||
notesElements = slideElement.querySelectorAll( 'aside.notes' ),
|
||||
fragmentElement = slideElement.querySelector( '.current-fragment' );
|
||||
|
||||
let messageData = {
|
||||
namespace: 'reveal-notes',
|
||||
type: 'state',
|
||||
notes: '',
|
||||
markdown: false,
|
||||
whitespace: 'normal',
|
||||
state: deck.getState()
|
||||
};
|
||||
|
||||
// Look for notes defined in a slide attribute
|
||||
if( slideElement.hasAttribute( 'data-notes' ) ) {
|
||||
messageData.notes = slideElement.getAttribute( 'data-notes' );
|
||||
messageData.whitespace = 'pre-wrap';
|
||||
}
|
||||
|
||||
// Look for notes defined in a fragment
|
||||
if( fragmentElement ) {
|
||||
let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
|
||||
if( fragmentNotes ) {
|
||||
messageData.notes = fragmentNotes.innerHTML;
|
||||
messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';
|
||||
|
||||
// Ignore other slide notes
|
||||
notesElements = null;
|
||||
}
|
||||
else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
|
||||
messageData.notes = fragmentElement.getAttribute( 'data-notes' );
|
||||
messageData.whitespace = 'pre-wrap';
|
||||
|
||||
// In case there are slide notes
|
||||
notesElements = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for notes defined in an aside element
|
||||
if( notesElements && notesElements.length ) {
|
||||
// Ignore notes inside of fragments since those are shown
|
||||
// individually when stepping through fragments
|
||||
notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );
|
||||
|
||||
messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );
|
||||
messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
|
||||
}
|
||||
|
||||
speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given event is from the same origin as the
|
||||
* current window.
|
||||
*/
|
||||
function isSameOriginEvent( event ) {
|
||||
|
||||
try {
|
||||
return window.location.origin === event.source.location.origin;
|
||||
}
|
||||
catch ( error ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onPostMessage( event ) {
|
||||
|
||||
// Only allow same-origin messages
|
||||
// (added 12/5/22 as a XSS safeguard)
|
||||
if( isSameOriginEvent( event ) ) {
|
||||
|
||||
try {
|
||||
let data = JSON.parse( event.data );
|
||||
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
|
||||
clearInterval( connectInterval );
|
||||
onConnected();
|
||||
}
|
||||
else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
|
||||
callRevealApi( data.methodName, data.arguments, data.callId );
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once we have established a connection to the notes
|
||||
* window.
|
||||
*/
|
||||
function onConnected() {
|
||||
|
||||
// Monitor events that trigger a change in state
|
||||
deck.on( 'slidechanged', post );
|
||||
deck.on( 'fragmentshown', post );
|
||||
deck.on( 'fragmenthidden', post );
|
||||
deck.on( 'overviewhidden', post );
|
||||
deck.on( 'overviewshown', post );
|
||||
deck.on( 'paused', post );
|
||||
deck.on( 'resumed', post );
|
||||
|
||||
// Post the initial state
|
||||
post();
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'notes',
|
||||
|
||||
init: function( reveal ) {
|
||||
|
||||
deck = reveal;
|
||||
|
||||
if( !/receiver/i.test( window.location.search ) ) {
|
||||
|
||||
// If the there's a 'notes' query set, open directly
|
||||
if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
|
||||
openSpeakerWindow();
|
||||
}
|
||||
else {
|
||||
// Keep listening for speaker view hearbeats. If we receive a
|
||||
// heartbeat from an orphaned window, reconnect it. This ensures
|
||||
// that we remain connected to the notes even if the presentation
|
||||
// is reloaded.
|
||||
window.addEventListener( 'message', event => {
|
||||
|
||||
if( !speakerWindow && typeof event.data === 'string' ) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse( event.data );
|
||||
}
|
||||
catch( error ) {}
|
||||
|
||||
if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
|
||||
reconnectSpeakerWindow( event.source );
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Open the notes when the 's' key is hit
|
||||
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
|
||||
openSpeakerWindow();
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
open: openSpeakerWindow
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
@@ -1,909 +0,0 @@
|
||||
<!--
|
||||
NOTE: You need to build the notes plugin after making changes to this file.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>reveal.js - Speaker View</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#current-slide,
|
||||
#upcoming-slide,
|
||||
#speaker-controls {
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
#current-slide iframe,
|
||||
#upcoming-slide iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#current-slide .label,
|
||||
#upcoming-slide .label {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#connection-status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
padding: 30% 20% 20% 20%;
|
||||
font-size: 18px;
|
||||
color: #222;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.overlay-element {
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 10px;
|
||||
text-shadow: none;
|
||||
background: rgba( 220, 220, 220, 0.8 );
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.overlay-element.interactive:hover {
|
||||
background: rgba( 220, 220, 220, 1 );
|
||||
}
|
||||
|
||||
#current-slide {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#upcoming-slide {
|
||||
position: absolute;
|
||||
width: 40%;
|
||||
height: 20%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Speaker controls */
|
||||
#speaker-controls {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
right: 0;
|
||||
width: 40%;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.speaker-controls-time.hidden,
|
||||
.speaker-controls-notes.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.speaker-controls-time .label,
|
||||
.speaker-controls-pace .label,
|
||||
.speaker-controls-notes .label {
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
font-size: 0.66em;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.speaker-controls-time, .speaker-controls-pace {
|
||||
border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 16px;
|
||||
padding-bottom: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.speaker-controls-time .reset-button {
|
||||
opacity: 0;
|
||||
float: right;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
.speaker-controls-time:hover .reset-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.speaker-controls-time .timer,
|
||||
.speaker-controls-time .clock {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.speaker-controls-time .timer,
|
||||
.speaker-controls-time .clock,
|
||||
.speaker-controls-time .pacing .hours-value,
|
||||
.speaker-controls-time .pacing .minutes-value,
|
||||
.speaker-controls-time .pacing .seconds-value {
|
||||
font-size: 1.9em;
|
||||
}
|
||||
|
||||
.speaker-controls-time .timer {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.speaker-controls-time .clock {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.speaker-controls-time span.mute {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.speaker-controls-time .pacing-title {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.speaker-controls-time .pacing.ahead {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.speaker-controls-time .pacing.on-track {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.speaker-controls-time .pacing.behind {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.speaker-controls-notes {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.speaker-controls-notes .value {
|
||||
margin-top: 5px;
|
||||
line-height: 1.4;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Layout selector */
|
||||
#speaker-layout {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: #222;
|
||||
z-index: 10;
|
||||
}
|
||||
#speaker-layout select {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0;
|
||||
box-shadow: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
font-size: 1em;
|
||||
background-color: transparent;
|
||||
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
#speaker-layout select:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Speaker layout: Wide */
|
||||
body[data-speaker-layout="wide"] #current-slide,
|
||||
body[data-speaker-layout="wide"] #upcoming-slide {
|
||||
width: 50%;
|
||||
height: 45%;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="wide"] #current-slide {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="wide"] #upcoming-slide {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="wide"] #speaker-controls {
|
||||
top: 45%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
/* Speaker layout: Tall */
|
||||
body[data-speaker-layout="tall"] #current-slide,
|
||||
body[data-speaker-layout="tall"] #upcoming-slide {
|
||||
width: 45%;
|
||||
height: 50%;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="tall"] #current-slide {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="tall"] #upcoming-slide {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="tall"] #speaker-controls {
|
||||
padding-top: 40px;
|
||||
top: 0;
|
||||
left: 45%;
|
||||
width: 55%;
|
||||
height: 100%;
|
||||
font-size: 1.45em;
|
||||
}
|
||||
|
||||
/* Tall: the timer moves into the quadrant freed by the smaller
|
||||
upcoming preview, so no space is wasted */
|
||||
body[data-speaker-layout="tall"] .speaker-controls-time {
|
||||
position: absolute;
|
||||
left: -81.8%;
|
||||
top: 75%;
|
||||
width: 81.8%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
/* Speaker layout: Notes only */
|
||||
body[data-speaker-layout="notes-only"] #current-slide,
|
||||
body[data-speaker-layout="notes-only"] #upcoming-slide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body[data-speaker-layout="notes-only"] #speaker-controls {
|
||||
padding-top: 40px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
body[data-speaker-layout="default"] #speaker-controls {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
body[data-speaker-layout="default"] #speaker-controls {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
body[data-speaker-layout="default"] #speaker-controls {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="connection-status">Loading speaker view...</div>
|
||||
|
||||
<div id="current-slide"></div>
|
||||
<div id="upcoming-slide"><span class="overlay-element label">Upcoming</span></div>
|
||||
<div id="speaker-controls">
|
||||
<div class="speaker-controls-time">
|
||||
<h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
|
||||
<div class="clock">
|
||||
<span class="clock-value">0:00 AM</span>
|
||||
</div>
|
||||
<div class="timer">
|
||||
<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
||||
<h4 class="label pacing-title" style="display: none">Pacing – Time to finish current slide</h4>
|
||||
<div class="pacing" style="display: none">
|
||||
<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="speaker-controls-notes hidden">
|
||||
<h4 class="label">Notes</h4>
|
||||
<div class="value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="speaker-layout" class="overlay-element interactive">
|
||||
<span class="speaker-layout-label"></span>
|
||||
<select class="speaker-layout-dropdown"></select>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
(function() {
|
||||
|
||||
var notes,
|
||||
notesValue,
|
||||
currentState,
|
||||
currentSlide,
|
||||
upcomingSlide,
|
||||
layoutLabel,
|
||||
layoutDropdown,
|
||||
pendingCalls = {},
|
||||
lastRevealApiCallId = 0,
|
||||
connected = false
|
||||
|
||||
var connectionStatus = document.querySelector( '#connection-status' );
|
||||
|
||||
var SPEAKER_LAYOUTS = {
|
||||
'default': 'Default',
|
||||
'wide': 'Wide',
|
||||
'tall': 'Tall',
|
||||
'notes-only': 'Notes only'
|
||||
};
|
||||
|
||||
setupLayout();
|
||||
|
||||
let openerOrigin;
|
||||
|
||||
try {
|
||||
openerOrigin = window.opener.location.origin;
|
||||
}
|
||||
catch ( error ) { console.warn( error ) }
|
||||
|
||||
// In order to prevent XSS, the speaker view will only run if its
|
||||
// opener has the same origin as itself
|
||||
if( window.location.origin !== openerOrigin ) {
|
||||
connectionStatus.innerHTML = 'Cross origin error.<br>The speaker window can only be opened from the same origin.';
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionTimeout = setTimeout( function() {
|
||||
connectionStatus.innerHTML = 'Error connecting to main window.<br>Please try closing and reopening the speaker view.';
|
||||
}, 5000 );
|
||||
|
||||
window.addEventListener( 'message', function( event ) {
|
||||
|
||||
// Validate the origin of all messages to avoid parsing messages
|
||||
// that aren't meant for us. Ignore when running off file:// so
|
||||
// that the speaker view continues to work without a web server.
|
||||
if( window.location.origin !== event.origin && window.location.origin !== 'file://' ) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout( connectionTimeout );
|
||||
connectionStatus.style.display = 'none';
|
||||
|
||||
var data = JSON.parse( event.data );
|
||||
|
||||
// The overview mode is only useful to the reveal.js instance
|
||||
// where navigation occurs so we don't sync it
|
||||
if( data.state ) delete data.state.overview;
|
||||
|
||||
// Messages sent by the notes plugin inside of the main window
|
||||
if( data && data.namespace === 'reveal-notes' ) {
|
||||
if( data.type === 'connect' ) {
|
||||
handleConnectMessage( data );
|
||||
}
|
||||
else if( data.type === 'state' ) {
|
||||
handleStateMessage( data );
|
||||
}
|
||||
else if( data.type === 'return' ) {
|
||||
pendingCalls[data.callId](data.result);
|
||||
delete pendingCalls[data.callId];
|
||||
}
|
||||
}
|
||||
// Messages sent by the reveal.js inside of the current slide preview
|
||||
else if( data && data.namespace === 'reveal' ) {
|
||||
if( /ready/.test( data.eventName ) ) {
|
||||
// Send a message back to notify that the handshake is complete
|
||||
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
|
||||
}
|
||||
else if( /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
|
||||
|
||||
dispatchStateToMainWindow( data.state );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
/**
|
||||
* Updates the presentation in the main window to match the state
|
||||
* of the presentation in the notes window.
|
||||
*/
|
||||
const dispatchStateToMainWindow = debounce(( state ) => {
|
||||
window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ state ]} ), '*' );
|
||||
}, 500);
|
||||
|
||||
/**
|
||||
* Asynchronously calls the Reveal.js API of the main frame.
|
||||
*/
|
||||
function callRevealApi( methodName, methodArguments, callback ) {
|
||||
|
||||
var callId = ++lastRevealApiCallId;
|
||||
pendingCalls[callId] = callback;
|
||||
window.opener.postMessage( JSON.stringify( {
|
||||
namespace: 'reveal-notes',
|
||||
type: 'call',
|
||||
callId: callId,
|
||||
methodName: methodName,
|
||||
arguments: methodArguments
|
||||
} ), '*' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main window is trying to establish a
|
||||
* connection.
|
||||
*/
|
||||
function handleConnectMessage( data ) {
|
||||
|
||||
if( connected === false ) {
|
||||
connected = true;
|
||||
|
||||
setupIframes( data );
|
||||
setupKeyboard();
|
||||
setupNotes();
|
||||
setupTimer();
|
||||
setupHeartbeat();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main window sends an updated state.
|
||||
*/
|
||||
function handleStateMessage( data ) {
|
||||
|
||||
// Store the most recently set state to avoid circular loops
|
||||
// applying the same state
|
||||
currentState = JSON.stringify( data.state );
|
||||
|
||||
// No need for updating the notes in case of fragment changes
|
||||
if ( data.notes ) {
|
||||
notes.classList.remove( 'hidden' );
|
||||
notesValue.style.whiteSpace = data.whitespace;
|
||||
if( data.markdown ) {
|
||||
notesValue.innerHTML = marked( data.notes );
|
||||
}
|
||||
else {
|
||||
notesValue.innerHTML = data.notes;
|
||||
}
|
||||
}
|
||||
else {
|
||||
notes.classList.add( 'hidden' );
|
||||
}
|
||||
|
||||
// Update the note slides
|
||||
currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
|
||||
upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
|
||||
upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
|
||||
|
||||
}
|
||||
|
||||
// Limit to max one state update per X ms
|
||||
handleStateMessage = debounce( handleStateMessage, 200 );
|
||||
|
||||
/**
|
||||
* Forward keyboard events to the current slide window.
|
||||
* This enables keyboard events to work even if focus
|
||||
* isn't set on the current slide iframe.
|
||||
*
|
||||
* Block F5 default handling, it reloads and disconnects
|
||||
* the speaker notes window.
|
||||
*/
|
||||
function setupKeyboard() {
|
||||
|
||||
document.addEventListener( 'keydown', function( event ) {
|
||||
if( event.keyCode === 116 || ( event.metaKey && event.keyCode === 82 ) ) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the preview iframes.
|
||||
*/
|
||||
function setupIframes( data ) {
|
||||
|
||||
var params = [
|
||||
'receiver',
|
||||
'progress=false',
|
||||
'history=false',
|
||||
'transition=none',
|
||||
'autoSlide=0',
|
||||
'backgroundTransition=none'
|
||||
].join( '&' );
|
||||
|
||||
var urlSeparator = /\?/.test(data.url) ? '&' : '?';
|
||||
var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
|
||||
var currentURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&postMessageEvents=true' + hash;
|
||||
var upcomingURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&controls=false' + hash;
|
||||
|
||||
currentSlide = document.createElement( 'iframe' );
|
||||
currentSlide.setAttribute( 'width', 1280 );
|
||||
currentSlide.setAttribute( 'height', 1024 );
|
||||
currentSlide.setAttribute( 'src', currentURL );
|
||||
document.querySelector( '#current-slide' ).appendChild( currentSlide );
|
||||
|
||||
upcomingSlide = document.createElement( 'iframe' );
|
||||
upcomingSlide.setAttribute( 'width', 640 );
|
||||
upcomingSlide.setAttribute( 'height', 512 );
|
||||
upcomingSlide.setAttribute( 'src', upcomingURL );
|
||||
document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notes UI.
|
||||
*/
|
||||
function setupNotes() {
|
||||
|
||||
notes = document.querySelector( '.speaker-controls-notes' );
|
||||
notesValue = document.querySelector( '.speaker-controls-notes .value' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We send out a heartbeat at all times to ensure we can
|
||||
* reconnect with the main presentation window after reloads.
|
||||
*/
|
||||
function setupHeartbeat() {
|
||||
|
||||
setInterval( () => {
|
||||
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
|
||||
}, 1000 );
|
||||
|
||||
}
|
||||
|
||||
function getTimings( callback ) {
|
||||
|
||||
callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {
|
||||
callRevealApi( 'getConfig', [], function ( config ) {
|
||||
var totalTime = config.totalTime;
|
||||
var minTimePerSlide = config.minimumTimePerSlide || 0;
|
||||
var defaultTiming = config.defaultTiming;
|
||||
if ((defaultTiming == null) && (totalTime == null)) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
// Setting totalTime overrides defaultTiming
|
||||
if (totalTime) {
|
||||
defaultTiming = 0;
|
||||
}
|
||||
var timings = [];
|
||||
for ( var i in slideAttributes ) {
|
||||
var slide = slideAttributes[ i ];
|
||||
var timing = defaultTiming;
|
||||
if( slide.hasOwnProperty( 'data-timing' )) {
|
||||
var t = slide[ 'data-timing' ];
|
||||
timing = parseInt(t);
|
||||
if( isNaN(timing) ) {
|
||||
console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming);
|
||||
timing = defaultTiming;
|
||||
}
|
||||
}
|
||||
timings.push(timing);
|
||||
}
|
||||
if ( totalTime ) {
|
||||
// After we've allocated time to individual slides, we summarize it and
|
||||
// subtract it from the total time
|
||||
var remainingTime = totalTime - timings.reduce( function(a, b) { return a + b; }, 0 );
|
||||
// The remaining time is divided by the number of slides that have 0 seconds
|
||||
// allocated at the moment, giving the average time-per-slide on the remaining slides
|
||||
var remainingSlides = (timings.filter( function(x) { return x == 0 }) ).length
|
||||
var timePerSlide = Math.round( remainingTime / remainingSlides, 0 )
|
||||
// And now we replace every zero-value timing with that average
|
||||
timings = timings.map( function(x) { return (x==0 ? timePerSlide : x) } );
|
||||
}
|
||||
var slidesUnderMinimum = timings.filter( function(x) { return (x < minTimePerSlide) } ).length
|
||||
if ( slidesUnderMinimum ) {
|
||||
message = "The pacing time for " + slidesUnderMinimum + " slide(s) is under the configured minimum of " + minTimePerSlide + " seconds. Check the data-timing attribute on individual slides, or consider increasing the totalTime or minimumTimePerSlide configuration options (or removing some slides).";
|
||||
alert(message);
|
||||
}
|
||||
callback( timings );
|
||||
} );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of seconds allocated for presenting
|
||||
* all slides up to and including this one.
|
||||
*/
|
||||
function getTimeAllocated( timings, callback ) {
|
||||
|
||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
||||
var allocated = 0;
|
||||
for (var i in timings.slice(0, currentSlide + 1)) {
|
||||
allocated += timings[i];
|
||||
}
|
||||
callback( allocated );
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the timer and clock and start updating them
|
||||
* at an interval.
|
||||
*/
|
||||
function setupTimer() {
|
||||
|
||||
var start = new Date(),
|
||||
timeEl = document.querySelector( '.speaker-controls-time' ),
|
||||
clockEl = timeEl.querySelector( '.clock-value' ),
|
||||
hoursEl = timeEl.querySelector( '.hours-value' ),
|
||||
minutesEl = timeEl.querySelector( '.minutes-value' ),
|
||||
secondsEl = timeEl.querySelector( '.seconds-value' ),
|
||||
pacingTitleEl = timeEl.querySelector( '.pacing-title' ),
|
||||
pacingEl = timeEl.querySelector( '.pacing' ),
|
||||
pacingHoursEl = pacingEl.querySelector( '.hours-value' ),
|
||||
pacingMinutesEl = pacingEl.querySelector( '.minutes-value' ),
|
||||
pacingSecondsEl = pacingEl.querySelector( '.seconds-value' );
|
||||
|
||||
var timings = null;
|
||||
getTimings( function ( _timings ) {
|
||||
|
||||
timings = _timings;
|
||||
if (_timings !== null) {
|
||||
pacingTitleEl.style.removeProperty('display');
|
||||
pacingEl.style.removeProperty('display');
|
||||
}
|
||||
|
||||
// Update once directly
|
||||
_updateTimer();
|
||||
|
||||
// Then update every second
|
||||
setInterval( _updateTimer, 1000 );
|
||||
|
||||
} );
|
||||
|
||||
|
||||
function _resetTimer() {
|
||||
|
||||
if (timings == null) {
|
||||
start = new Date();
|
||||
_updateTimer();
|
||||
}
|
||||
else {
|
||||
// Reset timer to beginning of current slide
|
||||
getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
|
||||
var slideEndTiming = slideEndTimingSeconds * 1000;
|
||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
||||
var currentSlideTiming = timings[currentSlide] * 1000;
|
||||
var previousSlidesTiming = slideEndTiming - currentSlideTiming;
|
||||
var now = new Date();
|
||||
start = new Date(now.getTime() - previousSlidesTiming);
|
||||
_updateTimer();
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
timeEl.addEventListener( 'click', function() {
|
||||
_resetTimer();
|
||||
return false;
|
||||
} );
|
||||
|
||||
function _displayTime( hrEl, minEl, secEl, time) {
|
||||
|
||||
var sign = Math.sign(time) == -1 ? "-" : "";
|
||||
time = Math.abs(Math.round(time / 1000));
|
||||
var seconds = time % 60;
|
||||
var minutes = Math.floor( time / 60 ) % 60 ;
|
||||
var hours = Math.floor( time / ( 60 * 60 )) ;
|
||||
hrEl.innerHTML = sign + zeroPadInteger( hours );
|
||||
if (hours == 0) {
|
||||
hrEl.classList.add( 'mute' );
|
||||
}
|
||||
else {
|
||||
hrEl.classList.remove( 'mute' );
|
||||
}
|
||||
minEl.innerHTML = ':' + zeroPadInteger( minutes );
|
||||
if (hours == 0 && minutes == 0) {
|
||||
minEl.classList.add( 'mute' );
|
||||
}
|
||||
else {
|
||||
minEl.classList.remove( 'mute' );
|
||||
}
|
||||
secEl.innerHTML = ':' + zeroPadInteger( seconds );
|
||||
}
|
||||
|
||||
function _updateTimer() {
|
||||
|
||||
var diff, hours, minutes, seconds,
|
||||
now = new Date();
|
||||
|
||||
diff = now.getTime() - start.getTime();
|
||||
|
||||
clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
|
||||
_displayTime( hoursEl, minutesEl, secondsEl, diff );
|
||||
if (timings !== null) {
|
||||
_updatePacing(diff);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function _updatePacing(diff) {
|
||||
|
||||
getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
|
||||
var slideEndTiming = slideEndTimingSeconds * 1000;
|
||||
|
||||
callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
|
||||
var currentSlideTiming = timings[currentSlide] * 1000;
|
||||
var timeLeftCurrentSlide = slideEndTiming - diff;
|
||||
if (timeLeftCurrentSlide < 0) {
|
||||
pacingEl.className = 'pacing behind';
|
||||
}
|
||||
else if (timeLeftCurrentSlide < currentSlideTiming) {
|
||||
pacingEl.className = 'pacing on-track';
|
||||
}
|
||||
else {
|
||||
pacingEl.className = 'pacing ahead';
|
||||
}
|
||||
_displayTime( pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the speaker view layout and layout selector.
|
||||
*/
|
||||
function setupLayout() {
|
||||
|
||||
layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
|
||||
layoutLabel = document.querySelector( '.speaker-layout-label' );
|
||||
|
||||
// Render the list of available layouts
|
||||
for( var id in SPEAKER_LAYOUTS ) {
|
||||
var option = document.createElement( 'option' );
|
||||
option.setAttribute( 'value', id );
|
||||
option.textContent = SPEAKER_LAYOUTS[ id ];
|
||||
layoutDropdown.appendChild( option );
|
||||
}
|
||||
|
||||
// Monitor the dropdown for changes
|
||||
layoutDropdown.addEventListener( 'change', function( event ) {
|
||||
|
||||
setLayout( layoutDropdown.value );
|
||||
|
||||
}, false );
|
||||
|
||||
// Restore any currently persisted layout
|
||||
setLayout( getLayout() );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new speaker view layout. The layout is persisted
|
||||
* in local storage.
|
||||
*/
|
||||
function setLayout( value ) {
|
||||
|
||||
var title = SPEAKER_LAYOUTS[ value ];
|
||||
|
||||
layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
|
||||
layoutDropdown.value = value;
|
||||
|
||||
document.body.setAttribute( 'data-speaker-layout', value );
|
||||
|
||||
// Persist locally
|
||||
if( supportsLocalStorage() ) {
|
||||
window.localStorage.setItem( 'reveal-speaker-layout', value );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the most recently set speaker layout
|
||||
* or our default layout if none has been set.
|
||||
*/
|
||||
function getLayout() {
|
||||
|
||||
if( supportsLocalStorage() ) {
|
||||
var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
|
||||
if( layout ) {
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the first record in the layouts hash
|
||||
for( var id in SPEAKER_LAYOUTS ) {
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function supportsLocalStorage() {
|
||||
|
||||
try {
|
||||
localStorage.setItem('test', 'test');
|
||||
localStorage.removeItem('test');
|
||||
return true;
|
||||
}
|
||||
catch( e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function zeroPadInteger( num ) {
|
||||
|
||||
var str = '00' + parseInt( num );
|
||||
return str.substring( str.length - 2 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the frequency at which a function can be called.
|
||||
*/
|
||||
function debounce( fn, ms ) {
|
||||
|
||||
var lastTime = 0,
|
||||
timeout;
|
||||
|
||||
return function() {
|
||||
|
||||
var args = arguments;
|
||||
var context = this;
|
||||
|
||||
clearTimeout( timeout );
|
||||
|
||||
var timeSinceLastCall = Date.now() - lastTime;
|
||||
if( timeSinceLastCall > ms ) {
|
||||
fn.apply( context, args );
|
||||
lastTime = Date.now();
|
||||
}
|
||||
else {
|
||||
timeout = setTimeout( function() {
|
||||
fn.apply( context, args );
|
||||
lastTime = Date.now();
|
||||
}, ms - timeSinceLastCall );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,243 +0,0 @@
|
||||
/*!
|
||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
||||
* by navigatating to that slide and highlighting it.
|
||||
*
|
||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
||||
*/
|
||||
|
||||
const Plugin = () => {
|
||||
|
||||
// The reveal.js instance this plugin is attached to
|
||||
let deck;
|
||||
|
||||
let searchElement;
|
||||
let searchButton;
|
||||
let searchInput;
|
||||
|
||||
let matchedSlides;
|
||||
let currentMatchedIndex;
|
||||
let searchboxDirty;
|
||||
let hilitor;
|
||||
|
||||
function render() {
|
||||
|
||||
searchElement = document.createElement( 'div' );
|
||||
searchElement.classList.add( 'searchbox' );
|
||||
searchElement.style.position = 'absolute';
|
||||
searchElement.style.top = '10px';
|
||||
searchElement.style.right = '10px';
|
||||
searchElement.style.zIndex = 10;
|
||||
|
||||
//embedded base64 search icon Designed by Sketchdock - http://www.sketchdock.com/:
|
||||
searchElement.innerHTML = `<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>
|
||||
</span>`;
|
||||
|
||||
searchInput = searchElement.querySelector( '.searchinput' );
|
||||
searchInput.style.width = '240px';
|
||||
searchInput.style.fontSize = '14px';
|
||||
searchInput.style.padding = '4px 6px';
|
||||
searchInput.style.color = '#000';
|
||||
searchInput.style.background = '#fff';
|
||||
searchInput.style.borderRadius = '2px';
|
||||
searchInput.style.border = '0';
|
||||
searchInput.style.outline = '0';
|
||||
searchInput.style.boxShadow = '0 2px 18px rgba(0, 0, 0, 0.2)';
|
||||
searchInput.style['-webkit-appearance'] = 'none';
|
||||
|
||||
deck.getRevealElement().appendChild( searchElement );
|
||||
|
||||
// searchButton.addEventListener( 'click', function(event) {
|
||||
// doSearch();
|
||||
// }, false );
|
||||
|
||||
searchInput.addEventListener( 'keyup', function( event ) {
|
||||
switch (event.keyCode) {
|
||||
case 13:
|
||||
event.preventDefault();
|
||||
doSearch();
|
||||
searchboxDirty = false;
|
||||
break;
|
||||
default:
|
||||
searchboxDirty = true;
|
||||
}
|
||||
}, false );
|
||||
|
||||
closeSearch();
|
||||
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
if( !searchElement ) render();
|
||||
|
||||
searchElement.style.display = 'inline';
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
if( !searchElement ) render();
|
||||
|
||||
searchElement.style.display = 'none';
|
||||
if(hilitor) hilitor.remove();
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
if( !searchElement ) render();
|
||||
|
||||
if (searchElement.style.display !== 'inline') {
|
||||
openSearch();
|
||||
}
|
||||
else {
|
||||
closeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
//if there's been a change in the search term, perform a new search:
|
||||
if (searchboxDirty) {
|
||||
var searchstring = searchInput.value;
|
||||
|
||||
if (searchstring === '') {
|
||||
if(hilitor) hilitor.remove();
|
||||
matchedSlides = null;
|
||||
}
|
||||
else {
|
||||
//find the keyword amongst the slides
|
||||
hilitor = new Hilitor("slidecontent");
|
||||
matchedSlides = hilitor.apply(searchstring);
|
||||
currentMatchedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedSlides) {
|
||||
//navigate to the next slide that has the keyword, wrapping to the first if necessary
|
||||
if (matchedSlides.length && (matchedSlides.length <= currentMatchedIndex)) {
|
||||
currentMatchedIndex = 0;
|
||||
}
|
||||
if (matchedSlides.length > currentMatchedIndex) {
|
||||
deck.slide(matchedSlides[currentMatchedIndex].h, matchedSlides[currentMatchedIndex].v);
|
||||
currentMatchedIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Original JavaScript code by Chirp Internet: www.chirp.com.au
|
||||
// Please acknowledge use of this code by including this header.
|
||||
// 2/2013 jon: modified regex to display any match, not restricted to word boundaries.
|
||||
function Hilitor(id, tag) {
|
||||
|
||||
var targetNode = document.getElementById(id) || document.body;
|
||||
var hiliteTag = tag || "EM";
|
||||
var skipTags = new RegExp("^(?:" + hiliteTag + "|SCRIPT|FORM)$");
|
||||
var colors = ["#ff6", "#a0ffff", "#9f9", "#f99", "#f6f"];
|
||||
var wordColor = [];
|
||||
var colorIdx = 0;
|
||||
var matchRegex = "";
|
||||
var matchingSlides = [];
|
||||
|
||||
this.setRegex = function(input)
|
||||
{
|
||||
input = input.trim();
|
||||
matchRegex = new RegExp("(" + input + ")","i");
|
||||
}
|
||||
|
||||
this.getRegex = function()
|
||||
{
|
||||
return matchRegex.toString().replace(/^\/\\b\(|\)\\b\/i$/g, "").replace(/\|/g, " ");
|
||||
}
|
||||
|
||||
// recursively apply word highlighting
|
||||
this.hiliteWords = function(node)
|
||||
{
|
||||
if(node == undefined || !node) return;
|
||||
if(!matchRegex) return;
|
||||
if(skipTags.test(node.nodeName)) return;
|
||||
|
||||
if(node.hasChildNodes()) {
|
||||
for(var i=0; i < node.childNodes.length; i++)
|
||||
this.hiliteWords(node.childNodes[i]);
|
||||
}
|
||||
if(node.nodeType == 3) { // NODE_TEXT
|
||||
var nv, regs;
|
||||
if((nv = node.nodeValue) && (regs = matchRegex.exec(nv))) {
|
||||
//find the slide's section element and save it in our list of matching slides
|
||||
var secnode = node;
|
||||
while (secnode != null && secnode.nodeName != 'SECTION') {
|
||||
secnode = secnode.parentNode;
|
||||
}
|
||||
|
||||
var slideIndex = deck.getIndices(secnode);
|
||||
var slidelen = matchingSlides.length;
|
||||
var alreadyAdded = false;
|
||||
for (var i=0; i < slidelen; i++) {
|
||||
if ( (matchingSlides[i].h === slideIndex.h) && (matchingSlides[i].v === slideIndex.v) ) {
|
||||
alreadyAdded = true;
|
||||
}
|
||||
}
|
||||
if (! alreadyAdded) {
|
||||
matchingSlides.push(slideIndex);
|
||||
}
|
||||
|
||||
if(!wordColor[regs[0].toLowerCase()]) {
|
||||
wordColor[regs[0].toLowerCase()] = colors[colorIdx++ % colors.length];
|
||||
}
|
||||
|
||||
var match = document.createElement(hiliteTag);
|
||||
match.appendChild(document.createTextNode(regs[0]));
|
||||
match.style.backgroundColor = wordColor[regs[0].toLowerCase()];
|
||||
match.style.fontStyle = "inherit";
|
||||
match.style.color = "#000";
|
||||
|
||||
var after = node.splitText(regs.index);
|
||||
after.nodeValue = after.nodeValue.substring(regs[0].length);
|
||||
node.parentNode.insertBefore(match, after);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// remove highlighting
|
||||
this.remove = function()
|
||||
{
|
||||
var arr = document.getElementsByTagName(hiliteTag);
|
||||
var el;
|
||||
while(arr.length && (el = arr[0])) {
|
||||
el.parentNode.replaceChild(el.firstChild, el);
|
||||
}
|
||||
};
|
||||
|
||||
// start highlighting at target node
|
||||
this.apply = function(input)
|
||||
{
|
||||
if(input == undefined || !input) return;
|
||||
this.remove();
|
||||
this.setRegex(input);
|
||||
this.hiliteWords(targetNode);
|
||||
return matchingSlides;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
id: 'search',
|
||||
|
||||
init: reveal => {
|
||||
|
||||
deck = reveal;
|
||||
deck.registerKeyboardShortcut( 'CTRL + Shift + F', 'Search' );
|
||||
|
||||
document.addEventListener( 'keydown', function( event ) {
|
||||
if( event.key == "F" && (event.ctrlKey || event.metaKey) ) { //Control+Shift+f
|
||||
event.preventDefault();
|
||||
toggleSearch();
|
||||
}
|
||||
}, false );
|
||||
|
||||
},
|
||||
|
||||
open: openSearch
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
@@ -1,7 +0,0 @@
|
||||
/*!
|
||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
||||
* by navigatating to that slide and highlighting it.
|
||||
*
|
||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
||||
*/
|
||||
const e=()=>{let e,t,n,l,i,o,r;function s(){t=document.createElement("div"),t.classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>\n\t\t</span>',n=t.querySelector(".searchinput"),n.style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){if(13===t.keyCode)t.preventDefault(),function(){if(o){var t=n.value;""===t?(r&&r.remove(),l=null):(r=new c("slidecontent"),l=r.apply(t),i=0)}l&&(l.length&&l.length<=i&&(i=0),l.length>i&&(e.slide(l[i].h,l[i].v),i++))}(),o=!1;else o=!0}),!1),d()}function a(){t||s(),t.style.display="inline",n.focus(),n.select()}function d(){t||s(),t.style.display="none",r&&r.remove()}function c(t,n){var l=document.getElementById(t)||document.body,i=n||"EM",o=new RegExp("^(?:"+i+"|SCRIPT|FORM)$"),r=["#ff6","#a0ffff","#9f9","#f99","#f6f"],s=[],a=0,d="",c=[];this.setRegex=function(e){e=e.trim(),d=new RegExp("("+e+")","i")},this.getRegex=function(){return d.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&d&&!o.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n<t.childNodes.length;n++)this.hiliteWords(t.childNodes[n]);var l,p;if(3==t.nodeType)if((l=t.nodeValue)&&(p=d.exec(l))){for(var u=t;null!=u&&"SECTION"!=u.nodeName;)u=u.parentNode;var h=e.getIndices(u),f=c.length,y=!1;for(n=0;n<f;n++)c[n].h===h.h&&c[n].v===h.v&&(y=!0);y||c.push(h),s[p[0].toLowerCase()]||(s[p[0].toLowerCase()]=r[a++%r.length]);var g=document.createElement(i);g.appendChild(document.createTextNode(p[0])),g.style.backgroundColor=s[p[0].toLowerCase()],g.style.fontStyle="inherit",g.style.color="#000";var v=t.splitText(p.index);v.nodeValue=v.nodeValue.substring(p[0].length),t.parentNode.insertBefore(g,v)}}},this.remove=function(){for(var e,t=document.getElementsByTagName(i);t.length&&(e=t[0]);)e.parentNode.replaceChild(e.firstChild,e)},this.apply=function(e){if(null!=e&&e)return this.remove(),this.setRegex(e),this.hiliteWords(l),c}}return{id:"search",init:n=>{e=n,e.registerKeyboardShortcut("CTRL + Shift + F","Search"),document.addEventListener("keydown",(function(e){"F"==e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),t||s(),"inline"!==t.style.display?a():d())}),!1)},open:a}};export{e as default};
|
||||
@@ -1,7 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealSearch=t()}(this,(function(){"use strict";
|
||||
/*!
|
||||
* Handles finding a text string anywhere in the slides and showing the next occurrence to the user
|
||||
* by navigatating to that slide and highlighting it.
|
||||
*
|
||||
* @author Jon Snyder <snyder.jon@gmail.com>, February 2013
|
||||
*/return()=>{let e,t,n,i,o,l,r;function s(){t=document.createElement("div"),t.classList.add("searchbox"),t.style.position="absolute",t.style.top="10px",t.style.right="10px",t.style.zIndex=10,t.innerHTML='<input type="search" class="searchinput" placeholder="Search..." style="vertical-align: top;"/>\n\t\t</span>',n=t.querySelector(".searchinput"),n.style.width="240px",n.style.fontSize="14px",n.style.padding="4px 6px",n.style.color="#000",n.style.background="#fff",n.style.borderRadius="2px",n.style.border="0",n.style.outline="0",n.style.boxShadow="0 2px 18px rgba(0, 0, 0, 0.2)",n.style["-webkit-appearance"]="none",e.getRevealElement().appendChild(t),n.addEventListener("keyup",(function(t){if(13===t.keyCode)t.preventDefault(),function(){if(l){var t=n.value;""===t?(r&&r.remove(),i=null):(r=new c("slidecontent"),i=r.apply(t),o=0)}i&&(i.length&&i.length<=o&&(o=0),i.length>o&&(e.slide(i[o].h,i[o].v),o++))}(),l=!1;else l=!0}),!1),d()}function a(){t||s(),t.style.display="inline",n.focus(),n.select()}function d(){t||s(),t.style.display="none",r&&r.remove()}function c(t,n){var i=document.getElementById(t)||document.body,o=n||"EM",l=new RegExp("^(?:"+o+"|SCRIPT|FORM)$"),r=["#ff6","#a0ffff","#9f9","#f99","#f6f"],s=[],a=0,d="",c=[];this.setRegex=function(e){e=e.trim(),d=new RegExp("("+e+")","i")},this.getRegex=function(){return d.toString().replace(/^\/\\b\(|\)\\b\/i$/g,"").replace(/\|/g," ")},this.hiliteWords=function(t){if(null!=t&&t&&d&&!l.test(t.nodeName)){if(t.hasChildNodes())for(var n=0;n<t.childNodes.length;n++)this.hiliteWords(t.childNodes[n]);var i,f;if(3==t.nodeType)if((i=t.nodeValue)&&(f=d.exec(i))){for(var u=t;null!=u&&"SECTION"!=u.nodeName;)u=u.parentNode;var p=e.getIndices(u),h=c.length,y=!1;for(n=0;n<h;n++)c[n].h===p.h&&c[n].v===p.v&&(y=!0);y||c.push(p),s[f[0].toLowerCase()]||(s[f[0].toLowerCase()]=r[a++%r.length]);var g=document.createElement(o);g.appendChild(document.createTextNode(f[0])),g.style.backgroundColor=s[f[0].toLowerCase()],g.style.fontStyle="inherit",g.style.color="#000";var v=t.splitText(f.index);v.nodeValue=v.nodeValue.substring(f[0].length),t.parentNode.insertBefore(g,v)}}},this.remove=function(){for(var e,t=document.getElementsByTagName(o);t.length&&(e=t[0]);)e.parentNode.replaceChild(e.firstChild,e)},this.apply=function(e){if(null!=e&&e)return this.remove(),this.setRegex(e),this.hiliteWords(i),c}}return{id:"search",init:n=>{e=n,e.registerKeyboardShortcut("CTRL + Shift + F","Search"),document.addEventListener("keydown",(function(e){"F"==e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),t||s(),"inline"!==t.style.display?a():d())}),!1)},open:a}}}));
|
||||
@@ -1,264 +0,0 @@
|
||||
/*!
|
||||
* reveal.js Zoom plugin
|
||||
*/
|
||||
const Plugin = {
|
||||
|
||||
id: 'zoom',
|
||||
|
||||
init: function( reveal ) {
|
||||
|
||||
reveal.getRevealElement().addEventListener( 'mousedown', function( event ) {
|
||||
var defaultModifier = /Linux/.test( window.navigator.platform ) ? 'ctrl' : 'alt';
|
||||
|
||||
var modifier = ( reveal.getConfig().zoomKey ? reveal.getConfig().zoomKey : defaultModifier ) + 'Key';
|
||||
var zoomLevel = ( reveal.getConfig().zoomLevel ? reveal.getConfig().zoomLevel : 2 );
|
||||
|
||||
if( event[ modifier ] && !reveal.isOverview() ) {
|
||||
event.preventDefault();
|
||||
|
||||
zoom.to({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
scale: zoomLevel,
|
||||
pan: false
|
||||
});
|
||||
}
|
||||
} );
|
||||
|
||||
},
|
||||
|
||||
destroy: () => {
|
||||
|
||||
zoom.reset();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default () => Plugin;
|
||||
|
||||
/*!
|
||||
* zoom.js 0.3 (modified for use with reveal.js)
|
||||
* http://lab.hakim.se/zoom-js
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
||||
*/
|
||||
var zoom = (function(){
|
||||
|
||||
// The current zoom level (scale)
|
||||
var level = 1;
|
||||
|
||||
// The current mouse position, used for panning
|
||||
var mouseX = 0,
|
||||
mouseY = 0;
|
||||
|
||||
// Timeout before pan is activated
|
||||
var panEngageTimeout = -1,
|
||||
panUpdateInterval = -1;
|
||||
|
||||
// Check for transform support so that we can fallback otherwise
|
||||
var supportsTransforms = 'transform' in document.body.style;
|
||||
|
||||
if( supportsTransforms ) {
|
||||
// The easing that will be applied when we zoom in/out
|
||||
document.body.style.transition = 'transform 0.8s ease';
|
||||
}
|
||||
|
||||
// Zoom out if the user hits escape
|
||||
document.addEventListener( 'keyup', function( event ) {
|
||||
if( level !== 1 && event.keyCode === 27 ) {
|
||||
zoom.out();
|
||||
}
|
||||
} );
|
||||
|
||||
// Monitor mouse movement for panning
|
||||
document.addEventListener( 'mousemove', function( event ) {
|
||||
if( level !== 1 ) {
|
||||
mouseX = event.clientX;
|
||||
mouseY = event.clientY;
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Applies the CSS required to zoom in, prefers the use of CSS3
|
||||
* transforms but falls back on zoom for IE.
|
||||
*
|
||||
* @param {Object} rect
|
||||
* @param {Number} scale
|
||||
*/
|
||||
function magnify( rect, scale ) {
|
||||
|
||||
var scrollOffset = getScrollOffset();
|
||||
|
||||
// Ensure a width/height is set
|
||||
rect.width = rect.width || 1;
|
||||
rect.height = rect.height || 1;
|
||||
|
||||
// Center the rect within the zoomed viewport
|
||||
rect.x -= ( window.innerWidth - ( rect.width * scale ) ) / 2;
|
||||
rect.y -= ( window.innerHeight - ( rect.height * scale ) ) / 2;
|
||||
|
||||
if( supportsTransforms ) {
|
||||
// Reset
|
||||
if( scale === 1 ) {
|
||||
document.body.style.transform = '';
|
||||
}
|
||||
// Scale
|
||||
else {
|
||||
var origin = scrollOffset.x +'px '+ scrollOffset.y +'px',
|
||||
transform = 'translate('+ -rect.x +'px,'+ -rect.y +'px) scale('+ scale +')';
|
||||
|
||||
document.body.style.transformOrigin = origin;
|
||||
document.body.style.transform = transform;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Reset
|
||||
if( scale === 1 ) {
|
||||
document.body.style.position = '';
|
||||
document.body.style.left = '';
|
||||
document.body.style.top = '';
|
||||
document.body.style.width = '';
|
||||
document.body.style.height = '';
|
||||
document.body.style.zoom = '';
|
||||
}
|
||||
// Scale
|
||||
else {
|
||||
document.body.style.position = 'relative';
|
||||
document.body.style.left = ( - ( scrollOffset.x + rect.x ) / scale ) + 'px';
|
||||
document.body.style.top = ( - ( scrollOffset.y + rect.y ) / scale ) + 'px';
|
||||
document.body.style.width = ( scale * 100 ) + '%';
|
||||
document.body.style.height = ( scale * 100 ) + '%';
|
||||
document.body.style.zoom = scale;
|
||||
}
|
||||
}
|
||||
|
||||
level = scale;
|
||||
|
||||
if( document.documentElement.classList ) {
|
||||
if( level !== 1 ) {
|
||||
document.documentElement.classList.add( 'zoomed' );
|
||||
}
|
||||
else {
|
||||
document.documentElement.classList.remove( 'zoomed' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pan the document when the mosue cursor approaches the edges
|
||||
* of the window.
|
||||
*/
|
||||
function pan() {
|
||||
var range = 0.12,
|
||||
rangeX = window.innerWidth * range,
|
||||
rangeY = window.innerHeight * range,
|
||||
scrollOffset = getScrollOffset();
|
||||
|
||||
// Up
|
||||
if( mouseY < rangeY ) {
|
||||
window.scroll( scrollOffset.x, scrollOffset.y - ( 1 - ( mouseY / rangeY ) ) * ( 14 / level ) );
|
||||
}
|
||||
// Down
|
||||
else if( mouseY > window.innerHeight - rangeY ) {
|
||||
window.scroll( scrollOffset.x, scrollOffset.y + ( 1 - ( window.innerHeight - mouseY ) / rangeY ) * ( 14 / level ) );
|
||||
}
|
||||
|
||||
// Left
|
||||
if( mouseX < rangeX ) {
|
||||
window.scroll( scrollOffset.x - ( 1 - ( mouseX / rangeX ) ) * ( 14 / level ), scrollOffset.y );
|
||||
}
|
||||
// Right
|
||||
else if( mouseX > window.innerWidth - rangeX ) {
|
||||
window.scroll( scrollOffset.x + ( 1 - ( window.innerWidth - mouseX ) / rangeX ) * ( 14 / level ), scrollOffset.y );
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollOffset() {
|
||||
return {
|
||||
x: window.scrollX !== undefined ? window.scrollX : window.pageXOffset,
|
||||
y: window.scrollY !== undefined ? window.scrollY : window.pageYOffset
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Zooms in on either a rectangle or HTML element.
|
||||
*
|
||||
* @param {Object} options
|
||||
* - element: HTML element to zoom in on
|
||||
* OR
|
||||
* - x/y: coordinates in non-transformed space to zoom in on
|
||||
* - width/height: the portion of the screen to zoom in on
|
||||
* - scale: can be used instead of width/height to explicitly set scale
|
||||
*/
|
||||
to: function( options ) {
|
||||
|
||||
// Due to an implementation limitation we can't zoom in
|
||||
// to another element without zooming out first
|
||||
if( level !== 1 ) {
|
||||
zoom.out();
|
||||
}
|
||||
else {
|
||||
options.x = options.x || 0;
|
||||
options.y = options.y || 0;
|
||||
|
||||
// If an element is set, that takes precedence
|
||||
if( !!options.element ) {
|
||||
// Space around the zoomed in element to leave on screen
|
||||
var padding = 20;
|
||||
var bounds = options.element.getBoundingClientRect();
|
||||
|
||||
options.x = bounds.left - padding;
|
||||
options.y = bounds.top - padding;
|
||||
options.width = bounds.width + ( padding * 2 );
|
||||
options.height = bounds.height + ( padding * 2 );
|
||||
}
|
||||
|
||||
// If width/height values are set, calculate scale from those values
|
||||
if( options.width !== undefined && options.height !== undefined ) {
|
||||
options.scale = Math.max( Math.min( window.innerWidth / options.width, window.innerHeight / options.height ), 1 );
|
||||
}
|
||||
|
||||
if( options.scale > 1 ) {
|
||||
options.x *= options.scale;
|
||||
options.y *= options.scale;
|
||||
|
||||
magnify( options, options.scale );
|
||||
|
||||
if( options.pan !== false ) {
|
||||
|
||||
// Wait with engaging panning as it may conflict with the
|
||||
// zoom transition
|
||||
panEngageTimeout = setTimeout( function() {
|
||||
panUpdateInterval = setInterval( pan, 1000 / 60 );
|
||||
}, 800 );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the document zoom state to its default.
|
||||
*/
|
||||
out: function() {
|
||||
clearTimeout( panEngageTimeout );
|
||||
clearInterval( panUpdateInterval );
|
||||
|
||||
magnify( { x: 0, y: 0 }, 1 );
|
||||
|
||||
level = 1;
|
||||
},
|
||||
|
||||
// Alias
|
||||
magnify: function( options ) { this.to( options ) },
|
||||
reset: function() { this.out() },
|
||||
|
||||
zoomLevel: function() {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,11 +0,0 @@
|
||||
/*!
|
||||
* reveal.js Zoom plugin
|
||||
*/
|
||||
const e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(t){var n=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:n)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;t[i]&&!e.isOverview()&&(t.preventDefault(),o.to({x:t.clientX,y:t.clientY,scale:d,pan:!1}))}))},destroy:()=>{o.reset()}};var t=()=>e,o=function(){var e=1,t=0,n=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,o){var n=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*o)/2,t.y-=(window.innerHeight-t.height*o)/2,l)if(1===o)document.body.style.transform="";else{var i=n.x+"px "+n.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+o+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===o?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(n.x+t.x)/o+"px",document.body.style.top=-(n.y+t.y)/o+"px",document.body.style.width=100*o+"%",document.body.style.height=100*o+"%",document.body.style.zoom=o);e=o,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var o=.12*window.innerWidth,i=.12*window.innerHeight,d=r();n<i?window.scroll(d.x,d.y-14/e*(1-n/i)):n>window.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-n)/i)*(14/e)),t<o?window.scroll(d.x-14/e*(1-t/o),d.y):t>window.innerWidth-o&&window.scroll(d.x+(1-(window.innerWidth-t)/o)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(t){1!==e&&27===t.keyCode&&o.out()})),document.addEventListener("mousemove",(function(o){1!==e&&(t=o.clientX,n=o.clientY)})),{to:function(t){if(1!==e)o.out();else{if(t.x=t.x||0,t.y=t.y||0,t.element){var n=t.element.getBoundingClientRect();t.x=n.left-20,t.y=n.top-20,t.width=n.width+40,t.height=n.height+40}void 0!==t.width&&void 0!==t.height&&(t.scale=Math.max(Math.min(window.innerWidth/t.width,window.innerHeight/t.height),1)),t.scale>1&&(t.x*=t.scale,t.y*=t.scale,s(t,t.scale),!1!==t.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();
|
||||
/*!
|
||||
* zoom.js 0.3 (modified for use with reveal.js)
|
||||
* http://lab.hakim.se/zoom-js
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
||||
*/export{t as default};
|
||||
@@ -1,11 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RevealZoom=t()}(this,(function(){"use strict";
|
||||
/*!
|
||||
* reveal.js Zoom plugin
|
||||
*/const e={id:"zoom",init:function(e){e.getRevealElement().addEventListener("mousedown",(function(o){var n=/Linux/.test(window.navigator.platform)?"ctrl":"alt",i=(e.getConfig().zoomKey?e.getConfig().zoomKey:n)+"Key",d=e.getConfig().zoomLevel?e.getConfig().zoomLevel:2;o[i]&&!e.isOverview()&&(o.preventDefault(),t.to({x:o.clientX,y:o.clientY,scale:d,pan:!1}))}))},destroy:()=>{t.reset()}};var t=function(){var e=1,o=0,n=0,i=-1,d=-1,l="transform"in document.body.style;function s(t,o){var n=r();if(t.width=t.width||1,t.height=t.height||1,t.x-=(window.innerWidth-t.width*o)/2,t.y-=(window.innerHeight-t.height*o)/2,l)if(1===o)document.body.style.transform="";else{var i=n.x+"px "+n.y+"px",d="translate("+-t.x+"px,"+-t.y+"px) scale("+o+")";document.body.style.transformOrigin=i,document.body.style.transform=d}else 1===o?(document.body.style.position="",document.body.style.left="",document.body.style.top="",document.body.style.width="",document.body.style.height="",document.body.style.zoom=""):(document.body.style.position="relative",document.body.style.left=-(n.x+t.x)/o+"px",document.body.style.top=-(n.y+t.y)/o+"px",document.body.style.width=100*o+"%",document.body.style.height=100*o+"%",document.body.style.zoom=o);e=o,document.documentElement.classList&&(1!==e?document.documentElement.classList.add("zoomed"):document.documentElement.classList.remove("zoomed"))}function c(){var t=.12*window.innerWidth,i=.12*window.innerHeight,d=r();n<i?window.scroll(d.x,d.y-14/e*(1-n/i)):n>window.innerHeight-i&&window.scroll(d.x,d.y+(1-(window.innerHeight-n)/i)*(14/e)),o<t?window.scroll(d.x-14/e*(1-o/t),d.y):o>window.innerWidth-t&&window.scroll(d.x+(1-(window.innerWidth-o)/t)*(14/e),d.y)}function r(){return{x:void 0!==window.scrollX?window.scrollX:window.pageXOffset,y:void 0!==window.scrollY?window.scrollY:window.pageYOffset}}return l&&(document.body.style.transition="transform 0.8s ease"),document.addEventListener("keyup",(function(o){1!==e&&27===o.keyCode&&t.out()})),document.addEventListener("mousemove",(function(t){1!==e&&(o=t.clientX,n=t.clientY)})),{to:function(o){if(1!==e)t.out();else{if(o.x=o.x||0,o.y=o.y||0,o.element){var n=o.element.getBoundingClientRect();o.x=n.left-20,o.y=n.top-20,o.width=n.width+40,o.height=n.height+40}void 0!==o.width&&void 0!==o.height&&(o.scale=Math.max(Math.min(window.innerWidth/o.width,window.innerHeight/o.height),1)),o.scale>1&&(o.x*=o.scale,o.y*=o.scale,s(o,o.scale),!1!==o.pan&&(i=setTimeout((function(){d=setInterval(c,1e3/60)}),800)))}},out:function(){clearTimeout(i),clearInterval(d),s({x:0,y:0},1),e=1},magnify:function(e){this.to(e)},reset:function(){this.out()},zoomLevel:function(){return e}}}();
|
||||
/*!
|
||||
* zoom.js 0.3 (modified for use with reveal.js)
|
||||
* http://lab.hakim.se/zoom-js
|
||||
* MIT licensed
|
||||
*
|
||||
* Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se
|
||||
*/return()=>e}));
|
||||
Reference in New Issue
Block a user