File size: 4,881 Bytes
5846c4a
c59bbe1
 
5846c4a
b8e1b6c
 
5846c4a
 
b8e1b6c
5846c4a
b8e1b6c
 
5846c4a
 
 
 
 
 
 
fee9c1e
5846c4a
 
72cfb5a
c59bbe1
72cfb5a
 
 
c59bbe1
72cfb5a
5846c4a
e329457
5846c4a
 
c1d1666
 
 
 
 
 
fee9c1e
e329457
fee9c1e
 
 
 
 
 
e329457
fee9c1e
 
 
 
 
c1d1666
fee9c1e
 
 
e329457
fee9c1e
 
72cfb5a
fee9c1e
 
 
 
c1d1666
 
 
fee9c1e
 
72cfb5a
03551cf
72cfb5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6281fd
72cfb5a
 
 
 
 
 
 
 
 
 
52307d3
 
 
 
 
 
 
 
 
72cfb5a
33aefb9
72cfb5a
33aefb9
 
 
 
 
 
 
 
 
 
 
 
72cfb5a
 
 
5846c4a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
---
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right' }
const { src, title, desc, frameless = false, align = 'left' } = Astro.props as Props;

// Load all .html embeds under src/content/embeds/** as strings (dev & build)
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;

function resolveFragment(requested: string): string | null {
  // Allow both "banner.html" and "embeds/banner.html"
  const needle = requested.replace(/^\/*/, '');
  for (const [key, html] of Object.entries(embeds)) {
    if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^embeds\//, ''))) {
      return html;
    }
  }
  return null;
}

const html = resolveFragment(src);
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
---
{ html ? (
  <figure class="html-embed">
    {title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
    <div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
      <div id={mountId} set:html={html} />
    </div>
    {desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
  </figure>
 ) : (
  <div><!-- Fragment not found: {src} --></div>
 ) }

<script type="module" is:inline>
  // Ensure global color palettes generator is loaded once per page
  import '../scripts/color-palettes.js';
  export {};
</script>

<script>
  // Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
  const scriptEl = document.currentScript;
  const mount = scriptEl ? scriptEl.previousElementSibling : null;
  const execute = () => {
    if (!mount) return;
    const scripts = mount.querySelectorAll('script');
    scripts.forEach(old => {
      // ignore non-executable types (e.g., application/json)
      if (old.type && old.type !== 'text/javascript' && old.type !== 'module' && old.type !== '') return;
      if (old.dataset.executed === 'true') return;
      old.dataset.executed = 'true';
      if (old.src) {
        const s = document.createElement('script');
        Array.from(old.attributes).forEach(attr => s.setAttribute(attr.name, attr.value));
        document.body.appendChild(s);
      } else {
        try {
          // run inline
          (0, eval)(old.text || '');
        } catch (e) {
          console.error('HtmlEmbed inline script error:', e);
        }
      }
    });
  };
  // Execute after DOM is parsed (ensures deferred module scripts are executed first)
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', execute, { once: true });
  else execute();
  </script>

<style>
  .html-embed { margin: 0; overflow: hidden; }
  .html-embed__title { 
    text-align: left; 
    font-weight: 600; 
    font-size: 0.95rem; 
    color: var(--text-color);
    margin: 0 0 6px 0;
  }
  .html-embed__card {
    background: var(--code-bg);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    padding: 8px;
  }
  .html-embed__card.is-frameless {
    background: transparent;
    border-color: transparent;
    padding: 0;
  }
  .html-embed__desc { 
    text-align: left; 
    font-size: 0.9rem; 
    color: var(--muted-color); 
    margin: 6px 0 0 0; 
  }
  @media (prefers-color-scheme: dark) {
    [data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
  }
  @media print {
    .html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }
    .html-embed__card { padding: 6px; }
    .html-embed__card.is-frameless { padding: 0; }
    .html-embed__card svg,
    .html-embed__card canvas,
    .html-embed__card img { max-width: 100% !important; height: auto !important; }
    .html-embed__card > div[id^="frag-"] { width: 100% !important; }
  }
  @media print {
    /* Avoid breaks inside embeds */
    .html-embed, .html-embed__card { break-inside: avoid; page-break-inside: avoid; }
    /* Constrain width and scale inner content */
    .html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; }
    .html-embed__card { padding: 6px; }
    .html-embed__card.is-frameless { padding: 0; }
    .html-embed__card svg,
    .html-embed__card canvas,
    .html-embed__card img,
    .html-embed__card video,
    .html-embed__card iframe { max-width: 100% !important; height: auto !important; }
    .html-embed__card > div[id^="frag-"] { width: 100% !important; max-width: 100% !important; }
    /* Center and constrain the banner (galaxy) when printing */
    .html-embed .d3-galaxy { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
  }
</style>