feat: claims for having the js parts in (not working, though)
This commit is contained in:
@@ -6,6 +6,7 @@ defmodule BDS.Generation do
|
|||||||
alias BDS.Generation.GeneratedFileHash
|
alias BDS.Generation.GeneratedFileHash
|
||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
alias BDS.Persistence
|
alias BDS.Persistence
|
||||||
|
alias BDS.PreviewAssets
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.Posts.Translation
|
alias BDS.Posts.Translation
|
||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
@@ -272,7 +273,14 @@ defmodule BDS.Generation do
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
core_outputs ++ single_outputs ++ archive_outputs ++ sitemap ++ pagefind_outputs
|
asset_outputs =
|
||||||
|
if :core in plan.sections do
|
||||||
|
PreviewAssets.generated_outputs()
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
core_outputs ++ single_outputs ++ archive_outputs ++ sitemap ++ pagefind_outputs ++ asset_outputs
|
||||||
end
|
end
|
||||||
|
|
||||||
defp disk_generated_files(project_id) do
|
defp disk_generated_files(project_id) do
|
||||||
|
|||||||
@@ -466,28 +466,39 @@ defmodule BDS.Preview do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp apply_preview_overrides(body, query_params) when is_binary(body) and is_map(query_params) do
|
defp apply_preview_overrides(body, query_params) when is_binary(body) and is_map(query_params) do
|
||||||
|
theme_override = normalize_pico_theme_override(query_params["theme"])
|
||||||
|
mode_override = normalize_mode_override(query_params["mode"])
|
||||||
|
|
||||||
body
|
body
|
||||||
|> override_pico_stylesheet_href(normalize_override(query_params["theme"]))
|
|> override_pico_stylesheet_href(theme_override)
|
||||||
|> override_html_attribute("data-theme", normalize_override(query_params["theme"]))
|
|> override_html_attribute("data-theme", mode_override)
|
||||||
|> override_html_attribute("data-mode", normalize_override(query_params["mode"]))
|
|> override_html_attribute("data-mode", mode_override)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp override_pico_stylesheet_href(body, nil), do: body
|
defp override_pico_stylesheet_href(body, nil), do: body
|
||||||
|
|
||||||
defp override_pico_stylesheet_href(body, theme) do
|
defp override_pico_stylesheet_href(body, theme) do
|
||||||
replacement =
|
Regex.replace(
|
||||||
case theme do
|
~r{/assets/pico(?:\.[a-z]+)?\.min\.css},
|
||||||
"default" -> "/assets/pico.min.css"
|
body,
|
||||||
value -> "/assets/pico.#{value}.min.css"
|
PreviewAssets.stylesheet_href(theme),
|
||||||
end
|
global: false
|
||||||
|
)
|
||||||
Regex.replace(~r{/assets/pico(?:\.[a-z]+)?\.min\.css}, body, replacement, global: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_override(nil), do: nil
|
defp normalize_override(nil), do: nil
|
||||||
defp normalize_override(""), do: nil
|
defp normalize_override(""), do: nil
|
||||||
defp normalize_override(value), do: String.trim(value)
|
defp normalize_override(value), do: String.trim(value)
|
||||||
|
|
||||||
|
defp normalize_pico_theme_override(value), do: normalize_override(value)
|
||||||
|
|
||||||
|
defp normalize_mode_override(value) do
|
||||||
|
case normalize_override(value) do
|
||||||
|
mode when mode in ["dark", "light"] -> mode
|
||||||
|
_other -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp override_html_attribute(body, _attribute, nil), do: body
|
defp override_html_attribute(body, _attribute, nil), do: body
|
||||||
|
|
||||||
defp override_html_attribute(body, attribute, value) do
|
defp override_html_attribute(body, attribute, value) do
|
||||||
@@ -509,8 +520,7 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
defp not_found_assigns(query_params) do
|
defp not_found_assigns(query_params) do
|
||||||
%{}
|
%{}
|
||||||
|> maybe_put_assign("html_theme_attribute", query_params["theme"], fn value -> ~s(data-theme="#{value}") end)
|
|> maybe_put_assign("pico_stylesheet_href", normalize_pico_theme_override(query_params["theme"]), &PreviewAssets.stylesheet_href/1)
|
||||||
|> maybe_put_assign("html_mode_attribute", query_params["mode"], fn value -> ~s(data-mode="#{value}") end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_assign(assigns, _key, nil, _mapper), do: assigns
|
defp maybe_put_assign(assigns, _key, nil, _mapper), do: assigns
|
||||||
|
|||||||
@@ -1,335 +1,70 @@
|
|||||||
defmodule BDS.PreviewAssets do
|
defmodule BDS.PreviewAssets do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@theme_tokens %{
|
@preview_root Application.app_dir(:bds, "priv/preview_assets")
|
||||||
"amber" => %{light_primary: "#876400", dark_primary: "#c79400"},
|
|
||||||
"blue" => %{light_primary: "#2060df", dark_primary: "#8999f9"},
|
|
||||||
"cyan" => %{light_primary: "#047878", dark_primary: "#0ab1b1"},
|
|
||||||
"fuchsia" => %{light_primary: "#c1208b", dark_primary: "#f869bf"},
|
|
||||||
"green" => %{light_primary: "#33790f", dark_primary: "#4eb31b"},
|
|
||||||
"grey" => %{light_primary: "#6a6a6a", dark_primary: "#9e9e9e"},
|
|
||||||
"indigo" => %{light_primary: "#655cd6", dark_primary: "#a294e5"},
|
|
||||||
"jade" => %{light_primary: "#007a50", dark_primary: "#00b478"},
|
|
||||||
"lime" => %{light_primary: "#577400", dark_primary: "#82ab00"},
|
|
||||||
"orange" => %{light_primary: "#bd3c13", dark_primary: "#f56b3d"},
|
|
||||||
"pink" => %{light_primary: "#c72259", dark_primary: "#f7708e"},
|
|
||||||
"pumpkin" => %{light_primary: "#9c5900", dark_primary: "#e48500"},
|
|
||||||
"purple" => %{light_primary: "#aa40bf", dark_primary: "#d47de4"},
|
|
||||||
"red" => %{light_primary: "#c52f21", dark_primary: "#f17961"},
|
|
||||||
"sand" => %{light_primary: "#6e6a60", dark_primary: "#a39e8f"},
|
|
||||||
"slate" => %{light_primary: "#5d6b89", dark_primary: "#909ebe"},
|
|
||||||
"violet" => %{light_primary: "#8352c5", dark_primary: "#b290d9"},
|
|
||||||
"yellow" => %{light_primary: "#756b00", dark_primary: "#ad9f00"},
|
|
||||||
"zinc" => %{light_primary: "#646b79", dark_primary: "#969eaf"}
|
|
||||||
}
|
|
||||||
|
|
||||||
@highlight_stylesheet """
|
|
||||||
.hljs { color: #e6edf3; background: transparent; display: block; overflow-x: auto; }
|
|
||||||
.hljs-keyword, .hljs-selector-tag, .hljs-literal { color: #ff7b72; }
|
|
||||||
.hljs-string, .hljs-attr { color: #a5d6ff; }
|
|
||||||
.hljs-number, .hljs-title, .hljs-section { color: #f2cc60; }
|
|
||||||
.hljs-comment, .hljs-quote { color: #8b949e; }
|
|
||||||
.hljs-built_in, .hljs-type, .hljs-symbol { color: #7ee787; }
|
|
||||||
"""
|
|
||||||
|
|
||||||
@highlight_script "window.hljs = window.hljs || { highlightElement: function () {} };"
|
|
||||||
|
|
||||||
@lightbox_script "window.lightbox = window.lightbox || { option: function () {}, init: function () {} };"
|
|
||||||
|
|
||||||
@calendar_stylesheet """
|
|
||||||
[data-blog-calendar-root] { min-height: 12rem; }
|
|
||||||
[data-blog-calendar-root] button { font: inherit; }
|
|
||||||
"""
|
|
||||||
|
|
||||||
@calendar_script """
|
|
||||||
(function () {
|
|
||||||
function Calendar() {}
|
|
||||||
Calendar.prototype.init = function () {};
|
|
||||||
window.VanillaCalendar = window.VanillaCalendar || Calendar;
|
|
||||||
window.VanillaCalendarPro = window.VanillaCalendarPro || Calendar;
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
@calendar_runtime """
|
|
||||||
(function () {
|
|
||||||
function toggle(panel, hidden) {
|
|
||||||
if (!panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
panel.setAttribute('hidden', 'hidden');
|
|
||||||
} else {
|
|
||||||
panel.removeAttribute('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
var toggleButton = document.querySelector('[data-blog-calendar-toggle]');
|
|
||||||
var closeButton = document.querySelector('[data-blog-calendar-close]');
|
|
||||||
var panel = document.querySelector('[data-blog-calendar-panel]');
|
|
||||||
var status = document.querySelector('[data-blog-calendar-status]');
|
|
||||||
|
|
||||||
if (!toggleButton || !panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleButton.addEventListener('click', function () {
|
|
||||||
var isHidden = panel.hasAttribute('hidden');
|
|
||||||
toggle(panel, !isHidden);
|
|
||||||
if (status && !status.dataset.previewReady) {
|
|
||||||
status.textContent = 'Preview calendar is unavailable in draft mode.';
|
|
||||||
status.dataset.previewReady = 'true';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (closeButton) {
|
|
||||||
closeButton.addEventListener('click', function () {
|
|
||||||
toggle(panel, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
@search_runtime """
|
|
||||||
(function () {
|
|
||||||
function setHidden(panel, hidden) {
|
|
||||||
if (!panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
panel.setAttribute('hidden', 'hidden');
|
|
||||||
} else {
|
|
||||||
panel.removeAttribute('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
var toggle = document.querySelector('[data-blog-search-toggle]');
|
|
||||||
var panel = document.querySelector('[data-blog-search-panel]');
|
|
||||||
if (!toggle || !panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle.addEventListener('click', function () {
|
|
||||||
setHidden(panel, !panel.hasAttribute('hidden'));
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', function (event) {
|
|
||||||
if (panel.hasAttribute('hidden')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panel.contains(event.target) || toggle.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHidden(panel, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
@tag_cloud_runtime """
|
|
||||||
(function () {
|
|
||||||
function init() {}
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
@d3_cloud_script """
|
|
||||||
(function () {
|
|
||||||
var layout = {
|
|
||||||
size: function () { return layout; },
|
|
||||||
words: function () { return layout; },
|
|
||||||
padding: function () { return layout; },
|
|
||||||
rotate: function () { return layout; },
|
|
||||||
font: function () { return layout; },
|
|
||||||
fontSize: function () { return layout; },
|
|
||||||
on: function () { return layout; },
|
|
||||||
start: function () { return layout; }
|
|
||||||
};
|
|
||||||
|
|
||||||
window.d3 = window.d3 || {};
|
|
||||||
window.d3.layout = window.d3.layout || {};
|
|
||||||
window.d3.layout.cloud = function () { return layout; };
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
def response(pathname) when is_binary(pathname) do
|
def response(pathname) when is_binary(pathname) do
|
||||||
case Regex.run(~r{^/assets/([^/]+)$}, pathname) do
|
pathname
|
||||||
[_, asset_name] -> build_asset_response(asset_name)
|
|> request_path()
|
||||||
_other -> :error
|
|> case do
|
||||||
|
{:ok, relative_path} ->
|
||||||
|
case File.read(Path.join(@preview_root, relative_path)) do
|
||||||
|
{:ok, body} -> {:ok, %{content_type: content_type(relative_path), body: body}}
|
||||||
|
{:error, _reason} -> :error
|
||||||
|
end
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_asset_response(asset_name) do
|
def generated_outputs do
|
||||||
case asset_payload(asset_name) do
|
["assets", "images"]
|
||||||
{:ok, content_type, body} -> {:ok, %{content_type: content_type, body: body}}
|
|> Enum.flat_map(fn directory ->
|
||||||
:error -> :error
|
@preview_root
|
||||||
|
|> Path.join(directory)
|
||||||
|
|> Path.join("**/*")
|
||||||
|
|> Path.wildcard(match_dot: false)
|
||||||
|
end)
|
||||||
|
|> Enum.filter(&File.regular?/1)
|
||||||
|
|> Enum.sort()
|
||||||
|
|> Enum.map(fn path ->
|
||||||
|
{Path.relative_to(path, @preview_root), File.read!(path)}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stylesheet_href(nil), do: "/assets/pico.min.css"
|
||||||
|
def stylesheet_href(""), do: "/assets/pico.min.css"
|
||||||
|
def stylesheet_href("default"), do: "/assets/pico.min.css"
|
||||||
|
def stylesheet_href(theme), do: "/assets/pico.#{theme}.min.css"
|
||||||
|
|
||||||
|
defp request_path(pathname) do
|
||||||
|
normalized = pathname |> URI.parse() |> Map.get(:path, pathname)
|
||||||
|
|
||||||
|
case String.split(normalized, "/", trim: true) do
|
||||||
|
[directory, filename] when directory in ["assets", "images"] ->
|
||||||
|
relative_path = Path.join(directory, filename)
|
||||||
|
|
||||||
|
if File.regular?(Path.join(@preview_root, relative_path)) do
|
||||||
|
{:ok, relative_path}
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp asset_payload(asset_name) do
|
defp content_type(path) do
|
||||||
case Regex.run(~r/^pico(?:\.([a-z]+))?\.min\.css$/, asset_name) do
|
case Path.extname(path) do
|
||||||
[_, theme] -> {:ok, "text/css", pico_stylesheet(theme)}
|
".css" -> "text/css"
|
||||||
[single] when single == asset_name -> {:ok, "text/css", pico_stylesheet(nil)}
|
".js" -> "application/javascript"
|
||||||
_other -> named_asset_payload(asset_name)
|
".png" -> "image/png"
|
||||||
|
".gif" -> "image/gif"
|
||||||
|
".jpg" -> "image/jpeg"
|
||||||
|
".jpeg" -> "image/jpeg"
|
||||||
|
_other -> "application/octet-stream"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp named_asset_payload("bds.css"), do: file_asset("bds.css", "text/css")
|
|
||||||
defp named_asset_payload("code-enhancements.js"), do: file_asset("code-enhancements.js", "application/javascript")
|
|
||||||
defp named_asset_payload("highlight.min.css"), do: {:ok, "text/css", @highlight_stylesheet}
|
|
||||||
defp named_asset_payload("highlight.min.js"), do: {:ok, "application/javascript", @highlight_script}
|
|
||||||
defp named_asset_payload("lightbox.min.css"), do: {:ok, "text/css", ""}
|
|
||||||
defp named_asset_payload("lightbox.min.js"), do: {:ok, "application/javascript", @lightbox_script}
|
|
||||||
defp named_asset_payload("vanilla-calendar.min.css"), do: {:ok, "text/css", @calendar_stylesheet}
|
|
||||||
defp named_asset_payload("vanilla-calendar.min.js"), do: {:ok, "application/javascript", @calendar_script}
|
|
||||||
defp named_asset_payload("calendar-runtime.js"), do: {:ok, "application/javascript", @calendar_runtime}
|
|
||||||
defp named_asset_payload("search-runtime.js"), do: {:ok, "application/javascript", @search_runtime}
|
|
||||||
defp named_asset_payload("tag-cloud.js"), do: {:ok, "application/javascript", @tag_cloud_runtime}
|
|
||||||
defp named_asset_payload("d3.layout.cloud.js"), do: {:ok, "application/javascript", @d3_cloud_script}
|
|
||||||
defp named_asset_payload(_asset_name), do: :error
|
|
||||||
|
|
||||||
defp file_asset(filename, content_type) do
|
|
||||||
case File.read(asset_path(filename)) do
|
|
||||||
{:ok, body} -> {:ok, content_type, body}
|
|
||||||
{:error, _reason} -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp asset_path(filename) do
|
|
||||||
Application.app_dir(:bds, "priv/preview_assets/#{filename}")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pico_stylesheet(theme) do
|
|
||||||
tokens = Map.get(@theme_tokens, theme || "", %{light_primary: "#2060df", dark_primary: "#8999f9"})
|
|
||||||
|
|
||||||
"""
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
--pico-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;
|
|
||||||
--pico-line-height: 1.6;
|
|
||||||
--pico-border-radius: 0.35rem;
|
|
||||||
--pico-background-color: #ffffff;
|
|
||||||
--pico-color: #1f2937;
|
|
||||||
--pico-muted-color: #5d6b89;
|
|
||||||
--pico-muted-border-color: #d6dde8;
|
|
||||||
--pico-card-background-color: #f7f9fc;
|
|
||||||
--pico-primary: #{tokens.light_primary};
|
|
||||||
--pico-primary-hover: #{tokens.light_primary};
|
|
||||||
--pico-primary-focus: rgba(32, 96, 223, 0.16);
|
|
||||||
--pico-primary-inverse: #ffffff;
|
|
||||||
--pico-code-background-color: #1f2937;
|
|
||||||
--pico-code-color: #e6edf3;
|
|
||||||
--pico-ins-color: #2f7a38;
|
|
||||||
--pico-del-color: #b74848;
|
|
||||||
--pico-form-element-background-color: #ffffff;
|
|
||||||
--pico-form-element-border-color: #c7d0dd;
|
|
||||||
--pico-form-element-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-mode='light'] {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-mode='dark'] {
|
|
||||||
color-scheme: dark;
|
|
||||||
--pico-background-color: #13171f;
|
|
||||||
--pico-color: #e6edf3;
|
|
||||||
--pico-muted-color: #909ebe;
|
|
||||||
--pico-muted-border-color: #2d3645;
|
|
||||||
--pico-card-background-color: #1b2230;
|
|
||||||
--pico-primary: #{tokens.dark_primary};
|
|
||||||
--pico-primary-hover: #{tokens.dark_primary};
|
|
||||||
--pico-primary-focus: rgba(137, 153, 249, 0.18);
|
|
||||||
--pico-code-background-color: #0f1520;
|
|
||||||
--pico-form-element-background-color: #13171f;
|
|
||||||
--pico-form-element-border-color: #364153;
|
|
||||||
--pico-form-element-color: #e6edf3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (prefers-color-scheme: dark) {
|
|
||||||
:root:not([data-mode='light']) {
|
|
||||||
color-scheme: dark;
|
|
||||||
--pico-background-color: #13171f;
|
|
||||||
--pico-color: #e6edf3;
|
|
||||||
--pico-muted-color: #909ebe;
|
|
||||||
--pico-muted-border-color: #2d3645;
|
|
||||||
--pico-card-background-color: #1b2230;
|
|
||||||
--pico-primary: #{tokens.dark_primary};
|
|
||||||
--pico-primary-hover: #{tokens.dark_primary};
|
|
||||||
--pico-primary-focus: rgba(137, 153, 249, 0.18);
|
|
||||||
--pico-code-background-color: #0f1520;
|
|
||||||
--pico-form-element-background-color: #13171f;
|
|
||||||
--pico-form-element-border-color: #364153;
|
|
||||||
--pico-form-element-color: #e6edf3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
html { font-family: var(--pico-font-family); background: var(--pico-background-color); color: var(--pico-color); }
|
|
||||||
body { margin: 0; font-family: inherit; background: var(--pico-background-color); color: var(--pico-color); line-height: var(--pico-line-height); }
|
|
||||||
h1, h2, h3, h4, h5, h6 { color: inherit; line-height: 1.2; margin: 0 0 0.75rem; }
|
|
||||||
p, ul, ol, pre, table, blockquote { margin: 0 0 1rem; }
|
|
||||||
a { color: var(--pico-primary); }
|
|
||||||
a:hover, a:focus-visible { color: var(--pico-primary-hover); }
|
|
||||||
button, [role='button'], input, textarea, select {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
button, [role='button'] {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
border: 1px solid var(--pico-form-element-border-color);
|
|
||||||
border-radius: var(--pico-border-radius);
|
|
||||||
background: var(--pico-card-background-color);
|
|
||||||
color: var(--pico-color);
|
|
||||||
padding: 0.45rem 0.8rem;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
input, textarea, select {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--pico-form-element-border-color);
|
|
||||||
border-radius: var(--pico-border-radius);
|
|
||||||
padding: 0.55rem 0.7rem;
|
|
||||||
background: var(--pico-form-element-background-color);
|
|
||||||
color: var(--pico-form-element-color);
|
|
||||||
}
|
|
||||||
code, pre, kbd {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
}
|
|
||||||
img { max-width: 100%; height: auto; }
|
|
||||||
hr { border: 0; border-top: 1px solid var(--pico-muted-border-color); }
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
|
||||||
th, td { padding: 0.45rem 0.6rem; border-bottom: 1px solid var(--pico-muted-border-color); text-align: left; }
|
|
||||||
blockquote {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 1rem;
|
|
||||||
border-left: 4px solid var(--pico-muted-border-color);
|
|
||||||
color: var(--pico-muted-color);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ defmodule BDS.Rendering do
|
|||||||
alias BDS.Media.Media, as: MediaAsset
|
alias BDS.Media.Media, as: MediaAsset
|
||||||
alias BDS.Menu
|
alias BDS.Menu
|
||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
|
alias BDS.PreviewAssets
|
||||||
alias BDS.PostLinks
|
alias BDS.PostLinks
|
||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
alias BDS.I18n
|
alias BDS.I18n
|
||||||
@@ -182,7 +183,7 @@ defmodule BDS.Rendering do
|
|||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:html_theme_attribute,
|
:html_theme_attribute,
|
||||||
Map.get(assigns, "html_theme_attribute", html_theme_attribute(metadata.pico_theme))
|
Map.get(assigns, "html_theme_attribute")
|
||||||
),
|
),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
alternate_links: alternate_links(canonical_post, project_id, main_language),
|
alternate_links: alternate_links(canonical_post, project_id, main_language),
|
||||||
@@ -240,7 +241,7 @@ defmodule BDS.Rendering do
|
|||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:html_theme_attribute,
|
:html_theme_attribute,
|
||||||
Map.get(assigns, "html_theme_attribute", html_theme_attribute(metadata.pico_theme))
|
Map.get(assigns, "html_theme_attribute")
|
||||||
),
|
),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
alternate_links: [],
|
alternate_links: [],
|
||||||
@@ -297,7 +298,7 @@ defmodule BDS.Rendering do
|
|||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
:html_theme_attribute,
|
:html_theme_attribute,
|
||||||
Map.get(assigns, "html_theme_attribute", html_theme_attribute(metadata.pico_theme))
|
Map.get(assigns, "html_theme_attribute")
|
||||||
),
|
),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
menu_items: menu_items(project_id),
|
menu_items: menu_items(project_id),
|
||||||
@@ -723,15 +724,7 @@ defmodule BDS.Rendering do
|
|||||||
defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
|
defp show_archive_range_heading?(%{kind: "date"}, _day_blocks), do: true
|
||||||
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
|
defp show_archive_range_heading?(_archive_context, _day_blocks), do: false
|
||||||
|
|
||||||
defp html_theme_attribute(nil), do: nil
|
defp default_pico_stylesheet_href(theme), do: PreviewAssets.stylesheet_href(theme)
|
||||||
defp html_theme_attribute(""), do: nil
|
|
||||||
defp html_theme_attribute("default"), do: nil
|
|
||||||
defp html_theme_attribute(theme), do: ~s(data-theme="#{theme}")
|
|
||||||
|
|
||||||
defp default_pico_stylesheet_href(nil), do: "/assets/pico.min.css"
|
|
||||||
defp default_pico_stylesheet_href(""), do: "/assets/pico.min.css"
|
|
||||||
defp default_pico_stylesheet_href("default"), do: "/assets/pico.min.css"
|
|
||||||
defp default_pico_stylesheet_href(theme), do: "/assets/pico.#{theme}.min.css"
|
|
||||||
|
|
||||||
defp href_for_language(""), do: "/"
|
defp href_for_language(""), do: "/"
|
||||||
defp href_for_language(prefix), do: prefix <> "/"
|
defp href_for_language(prefix), do: prefix <> "/"
|
||||||
|
|||||||
299
priv/preview_assets/assets/calendar-runtime.js
Normal file
299
priv/preview_assets/assets/calendar-runtime.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
(() => {
|
||||||
|
const button = document.querySelector('[data-blog-calendar-toggle]');
|
||||||
|
const panel = document.querySelector('[data-blog-calendar-panel]');
|
||||||
|
const closeButton = document.querySelector('[data-blog-calendar-close]');
|
||||||
|
const calendarRoot = document.querySelector('[data-blog-calendar-root]');
|
||||||
|
const status = document.querySelector('[data-blog-calendar-status]');
|
||||||
|
|
||||||
|
if (!button || !panel || !calendarRoot || !status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languagePrefix = document.documentElement.getAttribute('data-language-prefix') || '';
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
loading: panel.getAttribute('data-i18n-loading') || 'Loading calendar…',
|
||||||
|
error: panel.getAttribute('data-i18n-error') || 'Calendar data could not be loaded.',
|
||||||
|
};
|
||||||
|
|
||||||
|
let isInitialized = false;
|
||||||
|
let years = {};
|
||||||
|
let months = {};
|
||||||
|
let days = {};
|
||||||
|
let maxYearCount = 0;
|
||||||
|
let maxMonthCount = 0;
|
||||||
|
let maxDayCount = 0;
|
||||||
|
|
||||||
|
function pad2(value) {
|
||||||
|
return String(value).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCountMap(value) {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
for (const [key, rawCount] of Object.entries(value)) {
|
||||||
|
const count = Number(rawCount);
|
||||||
|
if (!Number.isFinite(count) || count <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map[key] = Math.floor(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeMaxCount(value) {
|
||||||
|
const counts = Object.values(value || {});
|
||||||
|
if (counts.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(...counts.map((count) => Number(count) || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHeatStyle(target, count, maxCount) {
|
||||||
|
if (!(target instanceof HTMLElement) || !Number.isFinite(count) || count <= 0 || !Number.isFinite(maxCount) || maxCount <= 0) {
|
||||||
|
target?.style?.setProperty('--blog-calendar-heat-alpha', '0');
|
||||||
|
target?.style?.setProperty('--blog-calendar-heat-hue', '210');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = Math.min(1, count / maxCount);
|
||||||
|
const hue = Math.round(210 - (210 * normalized));
|
||||||
|
const alpha = (0.30 + normalized * 0.65).toFixed(3);
|
||||||
|
|
||||||
|
target.style.setProperty('--blog-calendar-heat-hue', String(hue));
|
||||||
|
target.style.setProperty('--blog-calendar-heat-alpha', alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(pathname) {
|
||||||
|
if (!pathname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.assign(languagePrefix + pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInitialYearMonth() {
|
||||||
|
const initialYearRaw = button.getAttribute('data-blog-calendar-year');
|
||||||
|
const initialMonthRaw = button.getAttribute('data-blog-calendar-month');
|
||||||
|
|
||||||
|
const initialYear = Number(initialYearRaw);
|
||||||
|
const initialMonth = Number(initialMonthRaw);
|
||||||
|
|
||||||
|
let selectedYear = Number.isInteger(initialYear) && initialYear > 0 ? initialYear : null;
|
||||||
|
let selectedMonth = Number.isInteger(initialMonth) && initialMonth >= 1 && initialMonth <= 12
|
||||||
|
? (initialMonth - 1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!Number.isInteger(selectedYear) || !Number.isInteger(selectedMonth)) {
|
||||||
|
const rawPathname = window.location.pathname || '';
|
||||||
|
const pathname = languagePrefix && rawPathname.startsWith(languagePrefix + '/')
|
||||||
|
? rawPathname.slice(languagePrefix.length)
|
||||||
|
: rawPathname;
|
||||||
|
const parts = pathname.split('/').filter(Boolean);
|
||||||
|
const pathYear = Number(parts[0]);
|
||||||
|
const pathMonth = Number(parts[1]);
|
||||||
|
|
||||||
|
if (!Number.isInteger(selectedYear) && Number.isInteger(pathYear) && pathYear > 0 && String(parts[0]).length === 4) {
|
||||||
|
selectedYear = pathYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(selectedMonth) && Number.isInteger(pathMonth) && pathMonth >= 1 && pathMonth <= 12) {
|
||||||
|
selectedMonth = pathMonth - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { selectedYear, selectedMonth };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCalendarData() {
|
||||||
|
const response = await fetch('/calendar.json', { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('calendar.json request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = await response.json();
|
||||||
|
years = normalizeCountMap(parsed?.years);
|
||||||
|
months = normalizeCountMap(parsed?.months);
|
||||||
|
days = normalizeCountMap(parsed?.days);
|
||||||
|
maxYearCount = computeMaxCount(years);
|
||||||
|
maxMonthCount = computeMaxCount(months);
|
||||||
|
maxDayCount = computeMaxCount(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateFromClickEvent(event) {
|
||||||
|
if (!(event?.target instanceof Element)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateEl = event.target.closest('[data-vc-date]');
|
||||||
|
if (!(dateEl instanceof HTMLElement)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateEl.dataset.vcDate || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeCalendar() {
|
||||||
|
if (isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.textContent = labels.loading;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCalendarData();
|
||||||
|
|
||||||
|
const Calendar = window.VanillaCalendarPro?.Calendar;
|
||||||
|
if (typeof Calendar !== 'function') {
|
||||||
|
throw new Error('Vanilla Calendar Pro is unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialYearMonth = parseInitialYearMonth();
|
||||||
|
const calendarOptions = {
|
||||||
|
...(Number.isInteger(initialYearMonth.selectedYear) ? { selectedYear: initialYearMonth.selectedYear } : {}),
|
||||||
|
...(Number.isInteger(initialYearMonth.selectedMonth) ? { selectedMonth: initialYearMonth.selectedMonth } : {}),
|
||||||
|
onCreateDateEls(_self, dateEl) {
|
||||||
|
const dateKey = dateEl.dataset.vcDate || '';
|
||||||
|
const count = Number(days[dateKey] || 0);
|
||||||
|
const buttonEl = dateEl.querySelector('[data-vc-date-btn]');
|
||||||
|
|
||||||
|
if (!(buttonEl instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= 0) {
|
||||||
|
dateEl.removeAttribute('data-blog-calendar-has-posts');
|
||||||
|
applyHeatStyle(buttonEl, 0, maxDayCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||||
|
applyHeatStyle(buttonEl, count, maxDayCount);
|
||||||
|
},
|
||||||
|
onCreateMonthEls(self, monthEl) {
|
||||||
|
if (!(monthEl instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthIndex = Number(monthEl.dataset.vcMonthsMonth);
|
||||||
|
const selectedYear = Number(self?.context?.selectedYear);
|
||||||
|
if (!Number.isInteger(monthIndex) || !Number.isInteger(selectedYear)) {
|
||||||
|
monthEl.removeAttribute('data-blog-calendar-has-posts');
|
||||||
|
applyHeatStyle(monthEl, 0, maxMonthCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthKey = String(selectedYear) + '-' + pad2(monthIndex + 1);
|
||||||
|
const count = Number(months[monthKey] || 0);
|
||||||
|
if (count <= 0) {
|
||||||
|
monthEl.removeAttribute('data-blog-calendar-has-posts');
|
||||||
|
applyHeatStyle(monthEl, 0, maxMonthCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
monthEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||||
|
applyHeatStyle(monthEl, count, maxMonthCount);
|
||||||
|
},
|
||||||
|
onCreateYearEls(_self, yearEl) {
|
||||||
|
if (!(yearEl instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearValue = Number(yearEl.dataset.vcYearsYear);
|
||||||
|
if (!Number.isInteger(yearValue)) {
|
||||||
|
yearEl.removeAttribute('data-blog-calendar-has-posts');
|
||||||
|
applyHeatStyle(yearEl, 0, maxYearCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearKey = String(yearValue);
|
||||||
|
const count = Number(years[yearKey] || 0);
|
||||||
|
if (count <= 0) {
|
||||||
|
yearEl.removeAttribute('data-blog-calendar-has-posts');
|
||||||
|
applyHeatStyle(yearEl, 0, maxYearCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yearEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||||
|
applyHeatStyle(yearEl, count, maxYearCount);
|
||||||
|
},
|
||||||
|
onClickDate(_self, event) {
|
||||||
|
const dateKey = getDateFromClickEvent(event);
|
||||||
|
if (!dateKey || !days[dateKey]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = dateKey.split('-');
|
||||||
|
if (!year || !month || !day) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo('/' + year + '/' + month + '/' + day + '/');
|
||||||
|
},
|
||||||
|
onClickMonth(self) {
|
||||||
|
const selectedYear = Number(self?.context?.selectedYear);
|
||||||
|
const selectedMonth = Number(self?.context?.selectedMonth);
|
||||||
|
|
||||||
|
if (!Number.isInteger(selectedYear) || !Number.isInteger(selectedMonth)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthKey = String(selectedYear) + '-' + pad2(selectedMonth + 1);
|
||||||
|
if (!months[monthKey]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo('/' + String(selectedYear) + '/' + pad2(selectedMonth + 1) + '/');
|
||||||
|
},
|
||||||
|
onClickYear(self) {
|
||||||
|
const selectedYear = Number(self?.context?.selectedYear);
|
||||||
|
|
||||||
|
if (!Number.isInteger(selectedYear)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearKey = String(selectedYear);
|
||||||
|
if (!years[yearKey]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo('/' + String(selectedYear) + '/');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendar = new Calendar('[data-blog-calendar-root]', calendarOptions);
|
||||||
|
|
||||||
|
calendar.init();
|
||||||
|
status.textContent = '';
|
||||||
|
status.setAttribute('hidden', 'hidden');
|
||||||
|
isInitialized = true;
|
||||||
|
} catch {
|
||||||
|
status.textContent = labels.error;
|
||||||
|
status.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPanelOpen(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
panel.removeAttribute('hidden');
|
||||||
|
void initializeCalendar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.setAttribute('hidden', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const isHidden = panel.hasAttribute('hidden');
|
||||||
|
setPanelOpen(isHidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => {
|
||||||
|
setPanelOpen(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return document.execCommand('copy');
|
return document.execCommand('copy');
|
||||||
} catch (_) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch {
|
||||||
return fallbackCopy(value);
|
return fallbackCopy(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
try {
|
try {
|
||||||
highlighter.highlightElement(codeElement);
|
highlighter.highlightElement(codeElement);
|
||||||
codeElement.setAttribute('data-code-highlighted', 'true');
|
codeElement.setAttribute('data-code-highlighted', 'true');
|
||||||
} catch (_) {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,4 +133,4 @@
|
|||||||
} else {
|
} else {
|
||||||
initCodeBlocks();
|
initCodeBlocks();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
505
priv/preview_assets/assets/d3.layout.cloud.js
Normal file
505
priv/preview_assets/assets/d3.layout.cloud.js
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||||
|
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
|
||||||
|
// Algorithm due to Jonathan Feinberg, https://s3.amazonaws.com/static.mrfeinberg.com/bv_ch03.pdf
|
||||||
|
|
||||||
|
const dispatch = require("d3-dispatch").dispatch;
|
||||||
|
|
||||||
|
const RADIANS = Math.PI / 180;
|
||||||
|
|
||||||
|
const SPIRALS = {
|
||||||
|
archimedean: archimedeanSpiral,
|
||||||
|
rectangular: rectangularSpiral
|
||||||
|
};
|
||||||
|
|
||||||
|
const cw = 1 << 11 >> 5;
|
||||||
|
const ch = 1 << 11;
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
var size = [256, 256],
|
||||||
|
text = cloudText,
|
||||||
|
font = cloudFont,
|
||||||
|
fontSize = cloudFontSize,
|
||||||
|
fontStyle = cloudFontNormal,
|
||||||
|
fontWeight = cloudFontNormal,
|
||||||
|
rotate = cloudRotate,
|
||||||
|
padding = cloudPadding,
|
||||||
|
spiral = archimedeanSpiral,
|
||||||
|
words = [],
|
||||||
|
timeInterval = Infinity,
|
||||||
|
event = dispatch("word", "end"),
|
||||||
|
timer = null,
|
||||||
|
random = Math.random,
|
||||||
|
cloud = {},
|
||||||
|
canvas = cloudCanvas;
|
||||||
|
|
||||||
|
cloud.canvas = function(_) {
|
||||||
|
return arguments.length ? (canvas = functor(_), cloud) : canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.start = function() {
|
||||||
|
var contextAndRatio = getContext(canvas()),
|
||||||
|
board = zeroArray((size[0] >> 5) * size[1]),
|
||||||
|
bounds = null,
|
||||||
|
n = words.length,
|
||||||
|
i = -1,
|
||||||
|
tags = [],
|
||||||
|
data = words.map(function(d, i) {
|
||||||
|
d.text = text.call(this, d, i);
|
||||||
|
d.font = font.call(this, d, i);
|
||||||
|
d.style = fontStyle.call(this, d, i);
|
||||||
|
d.weight = fontWeight.call(this, d, i);
|
||||||
|
d.rotate = rotate.call(this, d, i);
|
||||||
|
d.size = ~~fontSize.call(this, d, i);
|
||||||
|
d.padding = padding.call(this, d, i);
|
||||||
|
return d;
|
||||||
|
}).sort(function(a, b) { return b.size - a.size; });
|
||||||
|
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(step, 0);
|
||||||
|
step();
|
||||||
|
|
||||||
|
return cloud;
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
var start = Date.now();
|
||||||
|
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
||||||
|
var d = data[i];
|
||||||
|
d.x = (size[0] * (random() + .5)) >> 1;
|
||||||
|
d.y = (size[1] * (random() + .5)) >> 1;
|
||||||
|
cloudSprite(contextAndRatio, d, data, i);
|
||||||
|
if (d.hasText && place(board, d, bounds)) {
|
||||||
|
tags.push(d);
|
||||||
|
event.call("word", cloud, d);
|
||||||
|
if (bounds) cloudBounds(bounds, d);
|
||||||
|
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
|
||||||
|
// Temporary hack
|
||||||
|
d.x -= size[0] >> 1;
|
||||||
|
d.y -= size[1] >> 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i >= n) {
|
||||||
|
cloud.stop();
|
||||||
|
event.call("end", cloud, tags, bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud.stop = function() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
for (const d of words) {
|
||||||
|
delete d.sprite;
|
||||||
|
}
|
||||||
|
return cloud;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getContext(canvas) {
|
||||||
|
const context = canvas.getContext("2d", {willReadFrequently: true});
|
||||||
|
|
||||||
|
canvas.width = canvas.height = 1;
|
||||||
|
const ratio = Math.sqrt(context.getImageData(0, 0, 1, 1).data.length >> 2);
|
||||||
|
canvas.width = (cw << 5) / ratio;
|
||||||
|
canvas.height = ch / ratio;
|
||||||
|
|
||||||
|
context.fillStyle = context.strokeStyle = "red";
|
||||||
|
|
||||||
|
return {context, ratio};
|
||||||
|
}
|
||||||
|
|
||||||
|
function place(board, tag, bounds) {
|
||||||
|
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
|
||||||
|
startX = tag.x,
|
||||||
|
startY = tag.y,
|
||||||
|
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
|
||||||
|
s = spiral(size),
|
||||||
|
dt = random() < .5 ? 1 : -1,
|
||||||
|
t = -dt,
|
||||||
|
dxdy,
|
||||||
|
dx,
|
||||||
|
dy;
|
||||||
|
|
||||||
|
while (dxdy = s(t += dt)) {
|
||||||
|
dx = ~~dxdy[0];
|
||||||
|
dy = ~~dxdy[1];
|
||||||
|
|
||||||
|
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
||||||
|
|
||||||
|
tag.x = startX + dx;
|
||||||
|
tag.y = startY + dy;
|
||||||
|
|
||||||
|
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
||||||
|
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
||||||
|
// TODO only check for collisions within current bounds.
|
||||||
|
if (!bounds || collideRects(tag, bounds)) {
|
||||||
|
if (!cloudCollide(tag, board, size[0])) {
|
||||||
|
var sprite = tag.sprite,
|
||||||
|
w = tag.width >> 5,
|
||||||
|
sw = size[0] >> 5,
|
||||||
|
lx = tag.x - (w << 4),
|
||||||
|
sx = lx & 0x7f,
|
||||||
|
msx = 32 - sx,
|
||||||
|
h = tag.y1 - tag.y0,
|
||||||
|
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||||
|
last;
|
||||||
|
for (var j = 0; j < h; j++) {
|
||||||
|
last = 0;
|
||||||
|
for (var i = 0; i <= w; i++) {
|
||||||
|
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
||||||
|
}
|
||||||
|
x += sw;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud.timeInterval = function(_) {
|
||||||
|
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.words = function(_) {
|
||||||
|
return arguments.length ? (words = _, cloud) : words;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.size = function(_) {
|
||||||
|
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.font = function(_) {
|
||||||
|
return arguments.length ? (font = functor(_), cloud) : font;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.fontStyle = function(_) {
|
||||||
|
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.fontWeight = function(_) {
|
||||||
|
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.rotate = function(_) {
|
||||||
|
return arguments.length ? (rotate = functor(_), cloud) : rotate;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.text = function(_) {
|
||||||
|
return arguments.length ? (text = functor(_), cloud) : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.spiral = function(_) {
|
||||||
|
return arguments.length ? (spiral = SPIRALS[_] || _, cloud) : spiral;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.fontSize = function(_) {
|
||||||
|
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.padding = function(_) {
|
||||||
|
return arguments.length ? (padding = functor(_), cloud) : padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.random = function(_) {
|
||||||
|
return arguments.length ? (random = _, cloud) : random;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloud.on = function() {
|
||||||
|
var value = event.on.apply(event, arguments);
|
||||||
|
return value === event ? cloud : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cloud;
|
||||||
|
};
|
||||||
|
|
||||||
|
function cloudText(d) {
|
||||||
|
return d.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudFont() {
|
||||||
|
return "serif";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudFontNormal() {
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudFontSize(d) {
|
||||||
|
return Math.sqrt(d.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudRotate() {
|
||||||
|
return (~~(random() * 6) - 3) * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudPadding() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches a monochrome sprite bitmap for the specified text.
|
||||||
|
// Load in batches for speed.
|
||||||
|
function cloudSprite(contextAndRatio, d, data, di) {
|
||||||
|
if (d.sprite) return;
|
||||||
|
var c = contextAndRatio.context,
|
||||||
|
ratio = contextAndRatio.ratio;
|
||||||
|
|
||||||
|
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
||||||
|
var x = 0,
|
||||||
|
y = 0,
|
||||||
|
maxh = 0,
|
||||||
|
n = data.length;
|
||||||
|
--di;
|
||||||
|
while (++di < n) {
|
||||||
|
d = data[di];
|
||||||
|
c.save();
|
||||||
|
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
||||||
|
const metrics = c.measureText(d.text);
|
||||||
|
const anchor = -Math.floor(metrics.width / 2);
|
||||||
|
let w = (metrics.width + 1) * ratio;
|
||||||
|
let h = d.size << 1;
|
||||||
|
if (d.rotate) {
|
||||||
|
var sr = Math.sin(d.rotate * RADIANS),
|
||||||
|
cr = Math.cos(d.rotate * RADIANS),
|
||||||
|
wcr = w * cr,
|
||||||
|
wsr = w * sr,
|
||||||
|
hcr = h * cr,
|
||||||
|
hsr = h * sr;
|
||||||
|
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
||||||
|
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
||||||
|
} else {
|
||||||
|
w = (w + 0x1f) >> 5 << 5;
|
||||||
|
}
|
||||||
|
if (h > maxh) maxh = h;
|
||||||
|
if (x + w >= (cw << 5)) {
|
||||||
|
x = 0;
|
||||||
|
y += maxh;
|
||||||
|
maxh = 0;
|
||||||
|
}
|
||||||
|
if (y + h >= ch) break;
|
||||||
|
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
||||||
|
if (d.rotate) c.rotate(d.rotate * RADIANS);
|
||||||
|
c.fillText(d.text, anchor, 0);
|
||||||
|
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, anchor, 0);
|
||||||
|
c.restore();
|
||||||
|
d.width = w;
|
||||||
|
d.height = h;
|
||||||
|
d.xoff = x;
|
||||||
|
d.yoff = y;
|
||||||
|
d.x1 = w >> 1;
|
||||||
|
d.y1 = h >> 1;
|
||||||
|
d.x0 = -d.x1;
|
||||||
|
d.y0 = -d.y1;
|
||||||
|
d.hasText = true;
|
||||||
|
x += w;
|
||||||
|
}
|
||||||
|
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
|
||||||
|
sprite = [];
|
||||||
|
while (--di >= 0) {
|
||||||
|
d = data[di];
|
||||||
|
if (!d.hasText) continue;
|
||||||
|
var w = d.width,
|
||||||
|
w32 = w >> 5,
|
||||||
|
h = d.y1 - d.y0;
|
||||||
|
// Zero the buffer
|
||||||
|
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
|
||||||
|
x = d.xoff;
|
||||||
|
if (x == null) return;
|
||||||
|
y = d.yoff;
|
||||||
|
var seen = 0,
|
||||||
|
seenRow = -1;
|
||||||
|
for (var j = 0; j < h; j++) {
|
||||||
|
for (var i = 0; i < w; i++) {
|
||||||
|
var k = w32 * j + (i >> 5),
|
||||||
|
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
||||||
|
sprite[k] |= m;
|
||||||
|
seen |= m;
|
||||||
|
}
|
||||||
|
if (seen) seenRow = j;
|
||||||
|
else {
|
||||||
|
d.y0++;
|
||||||
|
h--;
|
||||||
|
j--;
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.y1 = d.y0 + seenRow;
|
||||||
|
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mask-based collision detection.
|
||||||
|
function cloudCollide(tag, board, sw) {
|
||||||
|
sw >>= 5;
|
||||||
|
var sprite = tag.sprite,
|
||||||
|
w = tag.width >> 5,
|
||||||
|
lx = tag.x - (w << 4),
|
||||||
|
sx = lx & 0x7f,
|
||||||
|
msx = 32 - sx,
|
||||||
|
h = tag.y1 - tag.y0,
|
||||||
|
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||||
|
last;
|
||||||
|
for (var j = 0; j < h; j++) {
|
||||||
|
last = 0;
|
||||||
|
for (var i = 0; i <= w; i++) {
|
||||||
|
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
||||||
|
& board[x + i]) return true;
|
||||||
|
}
|
||||||
|
x += sw;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudBounds(bounds, d) {
|
||||||
|
var b0 = bounds[0],
|
||||||
|
b1 = bounds[1];
|
||||||
|
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
||||||
|
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
||||||
|
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
||||||
|
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collideRects(a, b) {
|
||||||
|
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function archimedeanSpiral(size) {
|
||||||
|
var e = size[0] / size[1];
|
||||||
|
return function(t) {
|
||||||
|
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectangularSpiral(size) {
|
||||||
|
var dy = 4,
|
||||||
|
dx = dy * size[0] / size[1],
|
||||||
|
x = 0,
|
||||||
|
y = 0;
|
||||||
|
return function(t) {
|
||||||
|
var sign = t < 0 ? -1 : 1;
|
||||||
|
// See triangular numbers: T_n = n * (n + 1) / 2.
|
||||||
|
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
||||||
|
case 0: x += dx; break;
|
||||||
|
case 1: y += dy; break;
|
||||||
|
case 2: x -= dx; break;
|
||||||
|
default: y -= dy; break;
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO reuse arrays?
|
||||||
|
function zeroArray(n) {
|
||||||
|
var a = [],
|
||||||
|
i = -1;
|
||||||
|
while (++i < n) a[i] = 0;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloudCanvas() {
|
||||||
|
return document.createElement("canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
function functor(d) {
|
||||||
|
return typeof d === "function" ? d : function() { return d; };
|
||||||
|
}
|
||||||
|
|
||||||
|
},{"d3-dispatch":2}],2:[function(require,module,exports){
|
||||||
|
// https://d3js.org/d3-dispatch/ v1.0.6 Copyright 2019 Mike Bostock
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||||
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||||
|
(global = global || self, factory(global.d3 = global.d3 || {}));
|
||||||
|
}(this, function (exports) { 'use strict';
|
||||||
|
|
||||||
|
var noop = {value: function() {}};
|
||||||
|
|
||||||
|
function dispatch() {
|
||||||
|
for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
|
||||||
|
if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
|
||||||
|
_[t] = [];
|
||||||
|
}
|
||||||
|
return new Dispatch(_);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dispatch(_) {
|
||||||
|
this._ = _;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTypenames(typenames, types) {
|
||||||
|
return typenames.trim().split(/^|\s+/).map(function(t) {
|
||||||
|
var name = "", i = t.indexOf(".");
|
||||||
|
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
|
||||||
|
if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
|
||||||
|
return {type: t, name: name};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatch.prototype = dispatch.prototype = {
|
||||||
|
constructor: Dispatch,
|
||||||
|
on: function(typename, callback) {
|
||||||
|
var _ = this._,
|
||||||
|
T = parseTypenames(typename + "", _),
|
||||||
|
t,
|
||||||
|
i = -1,
|
||||||
|
n = T.length;
|
||||||
|
|
||||||
|
// If no callback was specified, return the callback of the given type and name.
|
||||||
|
if (arguments.length < 2) {
|
||||||
|
while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a type was specified, set the callback for the given type and name.
|
||||||
|
// Otherwise, if a null callback was specified, remove callbacks of the given name.
|
||||||
|
if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
|
||||||
|
while (++i < n) {
|
||||||
|
if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
|
||||||
|
else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
copy: function() {
|
||||||
|
var copy = {}, _ = this._;
|
||||||
|
for (var t in _) copy[t] = _[t].slice();
|
||||||
|
return new Dispatch(copy);
|
||||||
|
},
|
||||||
|
call: function(type, that) {
|
||||||
|
if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
|
||||||
|
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
|
||||||
|
for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
|
||||||
|
},
|
||||||
|
apply: function(type, that, args) {
|
||||||
|
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
|
||||||
|
for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(type, name) {
|
||||||
|
for (var i = 0, n = type.length, c; i < n; ++i) {
|
||||||
|
if ((c = type[i]).name === name) {
|
||||||
|
return c.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(type, name, callback) {
|
||||||
|
for (var i = 0, n = type.length; i < n; ++i) {
|
||||||
|
if (type[i].name === name) {
|
||||||
|
type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (callback != null) type.push({name: name, value: callback});
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.dispatch = dispatch;
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
},{}]},{},[1])(1)
|
||||||
|
});
|
||||||
10
priv/preview_assets/assets/highlight.min.css
vendored
Normal file
10
priv/preview_assets/assets/highlight.min.css
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||||
|
Theme: GitHub Dark
|
||||||
|
Description: Dark theme as seen on github.com
|
||||||
|
Author: github.com
|
||||||
|
Maintainer: @Hirse
|
||||||
|
Updated: 2021-05-15
|
||||||
|
|
||||||
|
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||||
|
Current colors taken from GitHub's CSS
|
||||||
|
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||||
1244
priv/preview_assets/assets/highlight.min.js
vendored
Normal file
1244
priv/preview_assets/assets/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
priv/preview_assets/assets/lightbox.min.css
vendored
Normal file
1
priv/preview_assets/assets/lightbox.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.lb-loader,.lightbox{text-align:center;line-height:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;opacity:.8;display:none}.lightbox{position:absolute;left:0;width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-loader,.lb-nav{position:absolute;left:0}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{top:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;opacity:1}
|
||||||
25
priv/preview_assets/assets/lightbox.min.js
vendored
Normal file
25
priv/preview_assets/assets/lightbox.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.amber.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.amber.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.blue.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.blue.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.cyan.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.cyan.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.fuchsia.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.fuchsia.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.green.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.green.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.grey.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.grey.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.indigo.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.indigo.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.jade.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.jade.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.lime.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.lime.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.orange.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.orange.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.pink.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.pink.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.pumpkin.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.pumpkin.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.purple.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.purple.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.red.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.red.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.sand.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.sand.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.slate.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.slate.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.violet.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.violet.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.yellow.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.yellow.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.zinc.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.zinc.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
52
priv/preview_assets/assets/search-runtime.js
Normal file
52
priv/preview_assets/assets/search-runtime.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
(() => {
|
||||||
|
const toggle = document.querySelector('[data-blog-search-toggle]');
|
||||||
|
const panel = document.querySelector('[data-blog-search-panel]');
|
||||||
|
const root = document.querySelector('[data-blog-search-root]');
|
||||||
|
|
||||||
|
if (!toggle || !panel || !root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
function initSearch() {
|
||||||
|
if (initialized || typeof PagefindUI === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
|
||||||
|
new PagefindUI({
|
||||||
|
element: root,
|
||||||
|
showSubResults: true,
|
||||||
|
showImages: false,
|
||||||
|
translations: { placeholder: placeholder }
|
||||||
|
});
|
||||||
|
var input = root.querySelector('input');
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener('click', function() {
|
||||||
|
var isHidden = panel.hasAttribute('hidden');
|
||||||
|
if (isHidden) {
|
||||||
|
panel.removeAttribute('hidden');
|
||||||
|
initSearch();
|
||||||
|
} else {
|
||||||
|
panel.setAttribute('hidden', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!panel.hasAttribute('hidden') && !panel.contains(e.target) && !toggle.contains(e.target)) {
|
||||||
|
panel.setAttribute('hidden', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && !panel.hasAttribute('hidden')) {
|
||||||
|
panel.setAttribute('hidden', '');
|
||||||
|
toggle.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
272
priv/preview_assets/assets/tag-cloud.js
Normal file
272
priv/preview_assets/assets/tag-cloud.js
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
(function () {
|
||||||
|
function parseWords(rawWords) {
|
||||||
|
if (!rawWords || typeof rawWords !== 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawWords);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp01(value) {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCssColor(colorValue) {
|
||||||
|
if (typeof colorValue !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = colorValue.trim();
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
||||||
|
if (hexMatch) {
|
||||||
|
const hex = hexMatch[1];
|
||||||
|
if (hex.length === 3) {
|
||||||
|
return [
|
||||||
|
Number.parseInt(hex[0] + hex[0], 16),
|
||||||
|
Number.parseInt(hex[1] + hex[1], 16),
|
||||||
|
Number.parseInt(hex[2] + hex[2], 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
Number.parseInt(hex.slice(0, 2), 16),
|
||||||
|
Number.parseInt(hex.slice(2, 4), 16),
|
||||||
|
Number.parseInt(hex.slice(4, 6), 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgbMatch = value.match(/^rgba?\\(([^)]+)\\)$/i);
|
||||||
|
if (rgbMatch) {
|
||||||
|
const channels = rgbMatch[1]
|
||||||
|
.split(',')
|
||||||
|
.map((channel) => channel.trim())
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((channel) => {
|
||||||
|
if (channel.endsWith('%')) {
|
||||||
|
return Math.round((Number.parseFloat(channel) / 100) * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(Number.parseFloat(channel));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channels.length === 3 && channels.every((channel) => Number.isFinite(channel))) {
|
||||||
|
return channels.map((channel) => Math.max(0, Math.min(255, channel)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateColor(fromColor, toColor, t) {
|
||||||
|
return [
|
||||||
|
Math.round(fromColor[0] + ((toColor[0] - fromColor[0]) * t)),
|
||||||
|
Math.round(fromColor[1] + ((toColor[1] - fromColor[1]) * t)),
|
||||||
|
Math.round(fromColor[2] + ((toColor[2] - fromColor[2]) * t)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixColor(fromColor, toColor, weight) {
|
||||||
|
return interpolateColor(fromColor, toColor, clamp01(weight));
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorToCss(color) {
|
||||||
|
return 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPicoThemeStops() {
|
||||||
|
const style = window.getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
const blue = parseCssColor(style.getPropertyValue('--pico-secondary')) || [74, 99, 146];
|
||||||
|
const green = parseCssColor(style.getPropertyValue('--pico-ins-color')) || [53, 117, 56];
|
||||||
|
const red = parseCssColor(style.getPropertyValue('--pico-del-color')) || [183, 72, 72];
|
||||||
|
|
||||||
|
const yellow = mixColor(green, red, 0.45);
|
||||||
|
const orange = mixColor(green, red, 0.72);
|
||||||
|
|
||||||
|
return [blue, green, yellow, orange, red];
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateStops(stops, value) {
|
||||||
|
if (!Array.isArray(stops) || stops.length === 0) {
|
||||||
|
return 'currentColor';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stops.length === 1) {
|
||||||
|
return colorToCss(stops[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamped = clamp01(value);
|
||||||
|
const scaled = clamped * (stops.length - 1);
|
||||||
|
const lowerIndex = Math.floor(scaled);
|
||||||
|
const upperIndex = Math.min(stops.length - 1, lowerIndex + 1);
|
||||||
|
const localT = scaled - lowerIndex;
|
||||||
|
|
||||||
|
return colorToCss(interpolateColor(stops[lowerIndex], stops[upperIndex], localT));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveQuantileColorMap(words) {
|
||||||
|
const counts = Array.from(
|
||||||
|
new Set(words.map((word) => Number(word.count)).filter((count) => Number.isFinite(count)))
|
||||||
|
).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const quantiles = new Map();
|
||||||
|
if (counts.length === 0) {
|
||||||
|
return quantiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts.length === 1) {
|
||||||
|
quantiles.set(counts[0], 1);
|
||||||
|
return quantiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
counts.forEach((count, index) => {
|
||||||
|
quantiles.set(count, index / (counts.length - 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
return quantiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThemeAwareColors(words, container) {
|
||||||
|
const gammaRaw = Number.parseFloat(container.getAttribute('data-color-easing') || '0.7');
|
||||||
|
const gamma = Number.isFinite(gammaRaw) && gammaRaw > 0 ? gammaRaw : 0.7;
|
||||||
|
const quantiles = resolveQuantileColorMap(words);
|
||||||
|
const stops = getPicoThemeStops();
|
||||||
|
|
||||||
|
return words.map((word) => {
|
||||||
|
const count = Number(word.count);
|
||||||
|
const quantile = quantiles.get(count) ?? 0;
|
||||||
|
const eased = Math.pow(clamp01(quantile), gamma);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...word,
|
||||||
|
color: interpolateStops(stops, eased),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTagCloud(container) {
|
||||||
|
const cloudFactory = window.d3 && window.d3.layout && typeof window.d3.layout.cloud === 'function'
|
||||||
|
? window.d3.layout.cloud
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!cloudFactory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const langPrefix = document.documentElement.getAttribute('data-language-prefix') || '';
|
||||||
|
const rawWords = container.getAttribute('data-tag-cloud-words');
|
||||||
|
const words = parseWords(rawWords);
|
||||||
|
if (words.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorDistribution = container.getAttribute('data-color-distribution') || 'quantile';
|
||||||
|
const colorTheme = container.getAttribute('data-color-theme') || 'pico';
|
||||||
|
const coloredWords = colorDistribution === 'quantile' && colorTheme === 'pico'
|
||||||
|
? applyThemeAwareColors(words, container)
|
||||||
|
: words;
|
||||||
|
|
||||||
|
const width = Number.parseInt(container.getAttribute('data-width') || '900', 10) || 900;
|
||||||
|
const height = Number.parseInt(container.getAttribute('data-height') || '420', 10) || 420;
|
||||||
|
const orientation = container.getAttribute('data-orientation') || 'horizontal';
|
||||||
|
|
||||||
|
const resolveRotation = () => {
|
||||||
|
if (orientation === 'mixed-hv') {
|
||||||
|
return Math.random() < 0.5 ? 0 : 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === 'mixed-diagonal') {
|
||||||
|
const diagonalAngles = [-60, -30, 0, 30, 60, 90];
|
||||||
|
const index = Math.floor(Math.random() * diagonalAngles.length);
|
||||||
|
return diagonalAngles[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const svgNode = container.querySelector('svg.tag-cloud-canvas');
|
||||||
|
if (!svgNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (svgNode.firstChild) {
|
||||||
|
svgNode.removeChild(svgNode.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudFactory()
|
||||||
|
.size([width, height])
|
||||||
|
.words(coloredWords.map((word) => ({ ...word })))
|
||||||
|
.padding(4)
|
||||||
|
.rotate(() => resolveRotation())
|
||||||
|
.font('sans-serif')
|
||||||
|
.fontSize((word) => word.size)
|
||||||
|
.on('end', (layoutWords) => {
|
||||||
|
svgNode.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
||||||
|
svgNode.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||||
|
|
||||||
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
group.setAttribute('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')');
|
||||||
|
|
||||||
|
for (const word of layoutWords) {
|
||||||
|
const textNode = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
|
textNode.textContent = word.text;
|
||||||
|
textNode.setAttribute('text-anchor', 'middle');
|
||||||
|
textNode.setAttribute('transform', 'translate(' + word.x + ',' + word.y + ')rotate(' + (word.rotate || 0) + ')');
|
||||||
|
textNode.style.fontFamily = 'sans-serif';
|
||||||
|
textNode.style.fontSize = word.size + 'px';
|
||||||
|
textNode.style.fill = typeof word.color === 'string' && word.color.length > 0
|
||||||
|
? word.color
|
||||||
|
: 'currentColor';
|
||||||
|
textNode.style.cursor = 'pointer';
|
||||||
|
textNode.style.opacity = '0.9';
|
||||||
|
|
||||||
|
const titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||||
|
titleNode.textContent = word.text + ' (' + word.count + ')';
|
||||||
|
textNode.appendChild(titleNode);
|
||||||
|
|
||||||
|
textNode.addEventListener('click', () => {
|
||||||
|
if (word && typeof word.url === 'string' && word.url.length > 0) {
|
||||||
|
window.location.assign(langPrefix + word.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group.appendChild(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
svgNode.appendChild(group);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTagClouds() {
|
||||||
|
const containers = document.querySelectorAll('[data-tag-cloud="true"]');
|
||||||
|
containers.forEach((container) => drawTagCloud(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initTagClouds, { once: true });
|
||||||
|
} else {
|
||||||
|
initTagClouds();
|
||||||
|
}
|
||||||
|
})();
|
||||||
1
priv/preview_assets/assets/vanilla-calendar.min.css
vendored
Normal file
1
priv/preview_assets/assets/vanilla-calendar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
priv/preview_assets/assets/vanilla-calendar.min.js
vendored
Normal file
2
priv/preview_assets/assets/vanilla-calendar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
priv/preview_assets/images/close.png
Normal file
BIN
priv/preview_assets/images/close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 B |
BIN
priv/preview_assets/images/loading.gif
Normal file
BIN
priv/preview_assets/images/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
priv/preview_assets/images/next.png
Normal file
BIN
priv/preview_assets/images/next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
priv/preview_assets/images/prev.png
Normal file
BIN
priv/preview_assets/images/prev.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -6,6 +6,7 @@ defmodule BDS.GenerationTest do
|
|||||||
alias BDS.Media
|
alias BDS.Media
|
||||||
alias BDS.Metadata
|
alias BDS.Metadata
|
||||||
alias BDS.Posts
|
alias BDS.Posts
|
||||||
|
alias BDS.PreviewAssets
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@@ -113,7 +114,7 @@ defmodule BDS.GenerationTest do
|
|||||||
"de/pagefind/index.json",
|
"de/pagefind/index.json",
|
||||||
"de/pagefind/pagefind-ui.css",
|
"de/pagefind/pagefind-ui.css",
|
||||||
"de/pagefind/pagefind-ui.js"
|
"de/pagefind/pagefind-ui.js"
|
||||||
]
|
] ++ Enum.map(PreviewAssets.generated_outputs(), &elem(&1, 0))
|
||||||
|
|
||||||
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) ==
|
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) ==
|
||||||
Enum.sort(expected_paths)
|
Enum.sort(expected_paths)
|
||||||
@@ -125,6 +126,60 @@ defmodule BDS.GenerationTest do
|
|||||||
assert File.read!(Path.join([temp_dir, "html", "sitemap.xml"])) =~ "https://example.com/blog/"
|
assert File.read!(Path.join([temp_dir, "html", "sitemap.xml"])) =~ "https://example.com/blog/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "core generation writes local macro and preview assets for static html", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
public_url: "https://example.com/blog",
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en"],
|
||||||
|
pico_theme: "green"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, result} = BDS.Generation.generate_site(project.id, [:core])
|
||||||
|
|
||||||
|
expected_assets = [
|
||||||
|
"assets/pico.min.css",
|
||||||
|
"assets/pico.green.min.css",
|
||||||
|
"assets/lightbox.min.css",
|
||||||
|
"assets/lightbox.min.js",
|
||||||
|
"assets/highlight.min.css",
|
||||||
|
"assets/highlight.min.js",
|
||||||
|
"assets/code-enhancements.js",
|
||||||
|
"assets/d3.layout.cloud.js",
|
||||||
|
"assets/tag-cloud.js",
|
||||||
|
"assets/vanilla-calendar.min.css",
|
||||||
|
"assets/vanilla-calendar.min.js",
|
||||||
|
"assets/calendar-runtime.js",
|
||||||
|
"assets/search-runtime.js",
|
||||||
|
"assets/bds.css",
|
||||||
|
"images/prev.png",
|
||||||
|
"images/next.png",
|
||||||
|
"images/close.png",
|
||||||
|
"images/loading.gif"
|
||||||
|
]
|
||||||
|
|
||||||
|
generated_paths = Enum.map(result.generated_files, & &1.relative_path)
|
||||||
|
|
||||||
|
for relative_path <- expected_assets do
|
||||||
|
assert relative_path in generated_paths
|
||||||
|
assert File.exists?(Path.join([temp_dir, "html", relative_path]))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert File.read!(Path.join([temp_dir, "html", "assets", "calendar-runtime.js"])) =~
|
||||||
|
"loadCalendarData"
|
||||||
|
|
||||||
|
assert File.read!(Path.join([temp_dir, "html", "assets", "tag-cloud.js"])) =~
|
||||||
|
"data-tag-cloud-words"
|
||||||
|
|
||||||
|
assert File.read!(Path.join([temp_dir, "html", "assets", "bds.css"])) =~ ".blog-menu"
|
||||||
|
|
||||||
|
assert File.read!(Path.join([temp_dir, "html", "assets", "pico.green.min.css"])) =~
|
||||||
|
"color-scheme"
|
||||||
|
end
|
||||||
|
|
||||||
test "generation writes feed and atom entries with canonical URLs for published posts", %{
|
test "generation writes feed and atom entries with canonical URLs for published posts", %{
|
||||||
project: project,
|
project: project,
|
||||||
temp_dir: temp_dir
|
temp_dir: temp_dir
|
||||||
|
|||||||
@@ -88,6 +88,23 @@ defmodule BDS.PreviewTest do
|
|||||||
|
|
||||||
assert bds_css =~ ".blog-menu"
|
assert bds_css =~ ".blog-menu"
|
||||||
|
|
||||||
|
assert {:ok, %{body: calendar_runtime, content_type: "application/javascript"}} =
|
||||||
|
BDS.Preview.request(project.id, "/assets/calendar-runtime.js")
|
||||||
|
|
||||||
|
assert calendar_runtime =~ "loadCalendarData"
|
||||||
|
assert calendar_runtime =~ "window.location.assign"
|
||||||
|
|
||||||
|
assert {:ok, %{body: tag_cloud_runtime, content_type: "application/javascript"}} =
|
||||||
|
BDS.Preview.request(project.id, "/assets/tag-cloud.js")
|
||||||
|
|
||||||
|
assert tag_cloud_runtime =~ "data-tag-cloud-words"
|
||||||
|
|
||||||
|
assert {:ok, %{body: _prev_png, content_type: "image/png"}} =
|
||||||
|
BDS.Preview.request(project.id, "/images/prev.png")
|
||||||
|
|
||||||
|
assert {:ok, %{body: _loading_gif, content_type: "image/gif"}} =
|
||||||
|
BDS.Preview.request(project.id, "/images/loading.gif")
|
||||||
|
|
||||||
assert {:ok, %{body: "media body", content_type: "text/plain"}} =
|
assert {:ok, %{body: "media body", content_type: "text/plain"}} =
|
||||||
BDS.Preview.request(project.id, "/media/2026/04/image.txt")
|
BDS.Preview.request(project.id, "/media/2026/04/image.txt")
|
||||||
|
|
||||||
@@ -405,14 +422,14 @@ defmodule BDS.PreviewTest do
|
|||||||
assert {:ok, %{body: generated_html, content_type: "text/html"}} =
|
assert {:ok, %{body: generated_html, content_type: "text/html"}} =
|
||||||
BDS.Preview.request(project.id, "/?theme=amber&mode=dark")
|
BDS.Preview.request(project.id, "/?theme=amber&mode=dark")
|
||||||
|
|
||||||
assert generated_html =~ ~s(data-theme="amber")
|
assert generated_html =~ ~s(data-theme="dark")
|
||||||
assert generated_html =~ ~s(data-mode="dark")
|
assert generated_html =~ ~s(data-mode="dark")
|
||||||
assert generated_html =~ ~s(/assets/pico.amber.min.css)
|
assert generated_html =~ ~s(/assets/pico.amber.min.css)
|
||||||
|
|
||||||
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
|
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
|
||||||
BDS.Preview.preview_draft(project.id, "/draft/theme-draft?theme=amber&mode=dark", post.id)
|
BDS.Preview.preview_draft(project.id, "/draft/theme-draft?theme=amber&mode=dark", post.id)
|
||||||
|
|
||||||
assert draft_html =~ ~s(data-theme="amber")
|
assert draft_html =~ ~s(data-theme="dark")
|
||||||
assert draft_html =~ ~s(data-mode="dark")
|
assert draft_html =~ ~s(data-mode="dark")
|
||||||
assert draft_html =~ ~s(/assets/pico.amber.min.css)
|
assert draft_html =~ ~s(/assets/pico.amber.min.css)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user