From faa9599849b9492f5529c2e9329f0268f2f374b8 Mon Sep 17 00:00:00 2001 From: zack Date: Sat, 26 Oct 2024 21:41:22 -0400 Subject: [PATCH] svg preview renderer --- assets/css/app.css | 9 + assets/js/app.js | 11 +- assets/js/hooks/code_block_hook.js | 16 +- assets/js/hooks/searchable_dropdown.js | 32 --- assets/js/hooks/select.js | 186 ++++++++++++++ deps.nix | 134 ++++++++++ flake.nix | 3 +- lib/zoeyscomputer/gists/gist.ex | 18 +- .../components/code_block.ex | 70 ++--- .../components/core_components.ex | 11 +- .../components/layouts/app.html.heex | 2 +- .../components/searchable_dropdown.ex | 81 ------ .../controllers/gist_preview_controller.ex | 30 +++ .../live/gist_live/form_component.ex | 29 ++- lib/zoeyscomputer_web/live/gist_live/index.ex | 21 +- .../live/gist_live/index.html.heex | 4 +- .../live/gist_live/languages.ex | 15 ++ .../live/gist_live/og_image.ex | 243 ++++++++++++++++++ .../live/gist_live/select.ex | 125 +++++++++ lib/zoeyscomputer_web/live/gist_live/show.ex | 1 + .../live/gist_live/show.html.heex | 21 +- lib/zoeyscomputer_web/router.ex | 3 + mix.exs | 7 + mix.lock | 11 +- .../20241026212840_update_gists_code_type.exs | 9 + .../20241026223550_add_gists_info.exs | 10 + .../20241027003540_change_gists_id.exs | 26 ++ .../20241027005245_gists_author_field.exs | 9 + priv/static/fonts/Iosevka.css | 144 +++++------ 29 files changed, 1027 insertions(+), 254 deletions(-) delete mode 100644 assets/js/hooks/searchable_dropdown.js create mode 100644 assets/js/hooks/select.js delete mode 100644 lib/zoeyscomputer_web/components/searchable_dropdown.ex create mode 100644 lib/zoeyscomputer_web/controllers/gist_preview_controller.ex create mode 100644 lib/zoeyscomputer_web/live/gist_live/languages.ex create mode 100644 lib/zoeyscomputer_web/live/gist_live/og_image.ex create mode 100644 lib/zoeyscomputer_web/live/gist_live/select.ex create mode 100644 priv/repo/migrations/20241026212840_update_gists_code_type.exs create mode 100644 priv/repo/migrations/20241026223550_add_gists_info.exs create mode 100644 priv/repo/migrations/20241027003540_change_gists_id.exs create mode 100644 priv/repo/migrations/20241027005245_gists_author_field.exs diff --git a/assets/css/app.css b/assets/css/app.css index 378c8f9..69cfff7 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -3,3 +3,12 @@ @import "tailwindcss/utilities"; /* This file is for your main application CSS */ +code .highlighted { + @apply bg-ctp-mauve/10; + transition:background-color .5s; + margin:0 -24px; + padding:0 24px; + width:calc(100% + 48px); + display:inline-block +} + diff --git a/assets/js/app.js b/assets/js/app.js index b466707..a9f0021 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -22,16 +22,9 @@ import { Socket } from "phoenix"; import { LiveSocket } from "phoenix_live_view"; import topbar from "../vendor/topbar"; import CodeBlockHook from "./hooks/code_block_hook"; -import { - DropdownAnimation, - SearchableDropdown, -} from "./hooks/searchable_dropdown"; +import { SelectHook } from "./hooks/select"; -let Hooks = { - CodeBlockHook, - SearchableDropdown, - DropdownAnimation, -}; +let Hooks = { CodeBlockHook, SelectHook }; Hooks.ClickOutside = { mounted() { diff --git a/assets/js/hooks/code_block_hook.js b/assets/js/hooks/code_block_hook.js index 3868d18..ffa43f9 100644 --- a/assets/js/hooks/code_block_hook.js +++ b/assets/js/hooks/code_block_hook.js @@ -9,11 +9,10 @@ const CodeBlockHook = { this.el.dataset.highlightedLines || "[]", ); - console.log(code); - console.log("language", language); - const lines = code.split("\n"); + console.log(lines.length, lines); + // Convert line numbers to decorations const decorations = highlightedLines .map((line) => { @@ -40,7 +39,16 @@ const CodeBlockHook = { codeToHtml(code, { lang: language, theme: "catppuccin-mocha", - decorations, + transformers: [ + { + line(node, line) { + node.properties["data-line"] = line; + if (highlightedLines.includes(line)) { + this.addClassToHast(node, "highlighted"); + } + }, + }, + ], }).then((html) => { console.log(html); diff --git a/assets/js/hooks/searchable_dropdown.js b/assets/js/hooks/searchable_dropdown.js deleted file mode 100644 index 27d2f8e..0000000 --- a/assets/js/hooks/searchable_dropdown.js +++ /dev/null @@ -1,32 +0,0 @@ -SearchableDropdown = { - mounted() { - this.el.addEventListener("input", (e) => { - const query = e.target.value.toLowerCase(); - const dropdownId = this.el.dataset.dropdownId; - const optionsContainer = document.querySelector(`#${dropdownId}-options`); - const options = optionsContainer.querySelectorAll("li button"); - - options.forEach((option) => { - const text = option.textContent.toLowerCase(); - option.parentElement.style.display = text.includes(query) - ? "block" - : "none"; - }); - }); - }, -}; - -const DropdownAnimation = { - mounted() { - this.el.addEventListener("transitionend", (e) => { - if ( - e.propertyName === "opacity" && - this.el.classList.contains("fade-out") - ) { - this.el.classList.add("hidden"); - } - }); - }, -}; - -export { SearchableDropdown, DropdownAnimation }; diff --git a/assets/js/hooks/select.js b/assets/js/hooks/select.js new file mode 100644 index 0000000..3d6454d --- /dev/null +++ b/assets/js/hooks/select.js @@ -0,0 +1,186 @@ +const SelectHook = { + mounted() { + const container = this.el; + const trigger = container.querySelector('[id$="-trigger"]'); + const dropdown = container.querySelector('[id$="-dropdown"]'); + const search = container.querySelector('[id$="-search"]'); + const options = container.querySelector('[id$="-options"]'); + const hiddenInput = container.querySelector('input[type="hidden"]'); + const selectedText = container.querySelector('[id$="-selected"]'); + + const toggleDropdown = (event) => { + // Prevent the event from bubbling up to any parent form + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const isExpanded = dropdown.classList.contains("block"); + if (!isExpanded) { + dropdown.classList.remove("hidden"); + // Add animation classes + dropdown.classList.add( + "block", + "animate-in", + "fade-in", + "duration-200", + ); + search.value = ""; + filterOptions(""); + search.focus(); + } else { + // Add animation classes for hiding + dropdown.classList.remove("animate-in", "fade-in"); + dropdown.classList.add("animate-out", "fade-out"); + setTimeout(() => { + dropdown.classList.remove("block", "animate-out", "fade-out"); + dropdown.classList.add("hidden"); + }, 200); + } + }; + + const filterOptions = (query) => { + const items = options.querySelectorAll("li"); + items.forEach((item) => { + const text = item.textContent.toLowerCase(); + if (text.includes(query.toLowerCase())) { + item.style.display = ""; + } else { + item.style.display = "none"; + } + }); + }; + + const selectOption = (option, event) => { + // Prevent form submission + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const value = option.dataset.value; + const label = option.querySelector("span").textContent; + + // Update hidden input + hiddenInput.value = value; + + // Update visible text + selectedText.textContent = label; + + // Update check mark position + options.querySelectorAll("li").forEach((li) => { + const checkmark = li.querySelector("span:last-child"); + if (checkmark) checkmark.remove(); + + if (li.dataset.value === value) { + li.setAttribute("aria-selected", "true"); + const checkMarkSpan = document.createElement("span"); + checkMarkSpan.className = + "absolute inset-y-0 right-0 flex items-center pr-4 ctp-text-lavender"; + checkMarkSpan.innerHTML = ` + + + + `; + li.appendChild(checkMarkSpan); + } else { + li.setAttribute("aria-selected", "false"); + } + }); + + toggleDropdown(); + + // Dispatch change event without bubbling + const changeEvent = new Event("change", { bubbles: false }); + hiddenInput.dispatchEvent(changeEvent); + + // Push the value to the LiveView + this.pushEventTo(this.el, "change", { value: value }); + }; + + // Event Listeners + trigger.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + toggleDropdown(e); + }); + + search.addEventListener("input", (e) => { + e.preventDefault(); + e.stopPropagation(); + filterOptions(e.target.value); + }); + + options.querySelectorAll("li").forEach((option) => { + option.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + selectOption(option, e); + }); + }); + + // Prevent dropdowns from causing form submission + container.addEventListener("submit", (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // Close dropdown when clicking outside + document.addEventListener("click", (e) => { + if (!container.contains(e.target)) { + dropdown.classList.add("hidden"); + dropdown.classList.remove("block"); + } + }); + + // Keyboard navigation + container.addEventListener("keydown", (e) => { + const items = Array.from(options.querySelectorAll("li")).filter( + (li) => li.style.display !== "none", + ); + const currentIdx = items.findIndex((li) => li === document.activeElement); + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + if (currentIdx < items.length - 1) { + items[currentIdx + 1].focus(); + } + break; + case "ArrowUp": + e.preventDefault(); + if (currentIdx > 0) { + items[currentIdx - 1].focus(); + } + break; + case "Enter": + e.preventDefault(); + if (document.activeElement.tagName === "LI") { + selectOption(document.activeElement, e); + } + break; + case "Escape": + e.preventDefault(); + toggleDropdown(e); + break; + } + }); + + // Prevent the dropdown from closing when clicking inside it + dropdown.addEventListener("click", (e) => { + e.stopPropagation(); + }); + + // Prevent the search input from submitting the form + search.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); + }, +}; + +export { SelectHook }; diff --git a/deps.nix b/deps.nix index f21bb8c..fc38545 100644 --- a/deps.nix +++ b/deps.nix @@ -465,6 +465,108 @@ let beamDeps = [ decimal ]; }; + makeup = + let + version = "1.1.2"; + in + buildMix { + inherit version; + name = "makeup"; + + src = fetchHex { + inherit version; + pkg = "makeup"; + sha256 = "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"; + }; + + beamDeps = [ nimble_parsec ]; + }; + + makeup_elixir = + let + version = "0.16.2"; + in + buildMix { + inherit version; + name = "makeup_elixir"; + + src = fetchHex { + inherit version; + pkg = "makeup_elixir"; + sha256 = "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"; + }; + + beamDeps = [ makeup nimble_parsec ]; + }; + + makeup_erlang = + let + version = "0.1.5"; + in + buildMix { + inherit version; + name = "makeup_erlang"; + + src = fetchHex { + inherit version; + pkg = "makeup_erlang"; + sha256 = "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"; + }; + + beamDeps = [ makeup ]; + }; + + makeup_html = + let + version = "0.1.1"; + in + buildMix { + inherit version; + name = "makeup_html"; + + src = fetchHex { + inherit version; + pkg = "makeup_html"; + sha256 = "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"; + }; + + beamDeps = [ makeup ]; + }; + + makeup_js = + let + version = "0.1.0"; + in + buildMix { + inherit version; + name = "makeup_js"; + + src = fetchHex { + inherit version; + pkg = "makeup_js"; + sha256 = "3f0c1a5eb52c9737b1679c926574e83bb260ccdedf08b58ee96cca7c685dea75"; + }; + + beamDeps = [ makeup ]; + }; + + makeup_rust = + let + version = "0.3.0"; + in + buildMix { + inherit version; + name = "makeup_rust"; + + src = fetchHex { + inherit version; + pkg = "makeup_rust"; + sha256 = "1e79ee1995a3b9df9b7d90af7dc02525989efc41390a52065c23782c94ea94e1"; + }; + + beamDeps = [ makeup nimble_parsec ]; + }; + metrics = let version = "1.0.1"; @@ -557,6 +659,21 @@ let }; }; + nimble_parsec = + let + version = "1.4.0"; + in + buildMix { + inherit version; + name = "nimble_parsec"; + + src = fetchHex { + inherit version; + pkg = "nimble_parsec"; + sha256 = "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"; + }; + }; + nimble_pool = let version = "1.1.0"; @@ -636,6 +753,23 @@ let }; }; + phoenix_html_helpers = + let + version = "1.0.1"; + in + buildMix { + inherit version; + name = "phoenix_html_helpers"; + + src = fetchHex { + inherit version; + pkg = "phoenix_html_helpers"; + sha256 = "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"; + }; + + beamDeps = [ phoenix_html plug ]; + }; + phoenix_live_dashboard = let version = "0.8.4"; diff --git a/flake.nix b/flake.nix index f6fe2e0..10720e1 100644 --- a/flake.nix +++ b/flake.nix @@ -232,7 +232,7 @@ StartLimitBurst = 3; StartLimitInterval = 10; }; - path = [pkgs.bash pkgs.imagemagick]; + path = [pkgs.bash pkgs.imagemagick pkgs.librsvg]; }; }; }; @@ -318,6 +318,7 @@ nodejs mix2nix node2nix + librsvg imagemagick # Add the language server of your choice. inputs.lexical.packages.${system}.default diff --git a/lib/zoeyscomputer/gists/gist.ex b/lib/zoeyscomputer/gists/gist.ex index 3cd67df..24f911f 100644 --- a/lib/zoeyscomputer/gists/gist.ex +++ b/lib/zoeyscomputer/gists/gist.ex @@ -1,10 +1,16 @@ defmodule Zoeyscomputer.Gists.Gist do + alias Zoeyscomputer.IdGenerator use Ecto.Schema import Ecto.Changeset + alias Zoeyscomputer.Users.User + @primary_key {:id, :string, autogenerate: false} schema "gists" do field :code, :string field :lang, :string + field :title, :string + field :desc, :string + belongs_to :author, User timestamps(type: :utc_datetime) end @@ -12,7 +18,15 @@ defmodule Zoeyscomputer.Gists.Gist do @doc false def changeset(gist, attrs) do gist - |> cast(attrs, [:code, :lang]) - |> validate_required([:code, :lang]) + |> cast(attrs, [:code, :lang, :title, :desc]) + |> validate_required([:code, :lang, :title]) + |> put_new_id() + end + + defp put_new_id(changeset) do + case get_field(changeset, :id) do + nil -> put_change(changeset, :id, IdGenerator.generate(7)) + _id -> changeset + end end end diff --git a/lib/zoeyscomputer_web/components/code_block.ex b/lib/zoeyscomputer_web/components/code_block.ex index 2217398..bdf746f 100644 --- a/lib/zoeyscomputer_web/components/code_block.ex +++ b/lib/zoeyscomputer_web/components/code_block.ex @@ -1,6 +1,7 @@ defmodule ZoeyscomputerWeb.CodeBlock do use Phoenix.Component - alias Phoenix.LiveView.JS + + import ZoeyscomputerWeb.CoreComponents @moduledoc """ A code block component with syntax highlighting using Shiki. @@ -37,6 +38,7 @@ defmodule ZoeyscomputerWeb.CodeBlock do attr :code, :string, required: true attr :language, :string, required: true attr :title, :string, default: nil + attr :class, :string, default: "" attr :line_numbers, :boolean, default: false attr :highlighted_lines, :list, default: [] @@ -44,48 +46,46 @@ defmodule ZoeyscomputerWeb.CodeBlock do # Calculate the number of lines for line numbers assigns = assign(assigns, :num_lines, String.split(assigns.code, "\n") |> length()) + id = System.unique_integer() + ~H""" -
+
<%= if @title do %> -
-

<%= @title %>

+
+

<%= @title %>

<% end %> -
- <%= if @line_numbers do %> -
- <%= for line_num <- 1..@num_lines do %> - - <%= line_num %> - - <% end %> -
- <% end %> +
+
+ <%= if @line_numbers do %> +
+ <%= for line_num <- 1..@num_lines do %> + + <%= line_num %> + + <% end %> +
+ <% end %> -
-
<%= @code %>
+
+
<%= @code %>
+
- -
- +
+ <.copy_button id="code-copy-btn" content={"code-block-#{id}"} />
""" diff --git a/lib/zoeyscomputer_web/components/core_components.ex b/lib/zoeyscomputer_web/components/core_components.ex index 615bc5d..979fec2 100644 --- a/lib/zoeyscomputer_web/components/core_components.ex +++ b/lib/zoeyscomputer_web/components/core_components.ex @@ -235,8 +235,9 @@ defmodule ZoeyscomputerWeb.CoreComponents do diff --git a/lib/zoeyscomputer_web/components/layouts/app.html.heex b/lib/zoeyscomputer_web/components/layouts/app.html.heex index 3f97e57..1116e0b 100644 --- a/lib/zoeyscomputer_web/components/layouts/app.html.heex +++ b/lib/zoeyscomputer_web/components/layouts/app.html.heex @@ -1,5 +1,5 @@
-
+
<.flash_group flash={@flash} /> <%= @inner_content %>
diff --git a/lib/zoeyscomputer_web/components/searchable_dropdown.ex b/lib/zoeyscomputer_web/components/searchable_dropdown.ex deleted file mode 100644 index 94a5152..0000000 --- a/lib/zoeyscomputer_web/components/searchable_dropdown.ex +++ /dev/null @@ -1,81 +0,0 @@ -defmodule ZoeyscomputerWeb.SearchableDropdown do - use Phoenix.Component - alias Phoenix.LiveView.JS - - attr :id, :string, required: true - attr :options, :list, required: true - attr :selected, :string, default: nil - attr :class, :string, default: nil - attr :name, :string, required: true - attr :form, :any, required: true - - def searchable_dropdown(assigns) do - ~H""" -
-
- - - -
-
- """ - end -end diff --git a/lib/zoeyscomputer_web/controllers/gist_preview_controller.ex b/lib/zoeyscomputer_web/controllers/gist_preview_controller.ex new file mode 100644 index 0000000..a15950e --- /dev/null +++ b/lib/zoeyscomputer_web/controllers/gist_preview_controller.ex @@ -0,0 +1,30 @@ +defmodule ZoeyscomputerWeb.GistPreviewController do + use ZoeyscomputerWeb, :controller + + require Logger + alias Zoeyscomputer.Gists + + def show(conn, %{"id" => id}) do + gist = Gists.get_gist!(id) + + {:ok, webp} = ZoeyscomputerWeb.GistLive.OgImage.get_webp(gist) + + conn + |> put_resp_content_type("image/webp") + |> put_resp_header("cache-control", "public, max-age=300") + |> send_resp(200, webp) + |> halt() + end + + def raw(conn, %{"id" => id}) do + gist = Gists.get_gist!(id) + + webp = ZoeyscomputerWeb.GistLive.OgImage.generate_preview(gist) + + conn + |> put_resp_content_type("image/svg+xml") + |> put_resp_header("cache-control", "public, max-age=300") + |> send_resp(200, webp) + |> halt() + end +end diff --git a/lib/zoeyscomputer_web/live/gist_live/form_component.ex b/lib/zoeyscomputer_web/live/gist_live/form_component.ex index ad6039b..f1d51c3 100644 --- a/lib/zoeyscomputer_web/live/gist_live/form_component.ex +++ b/lib/zoeyscomputer_web/live/gist_live/form_component.ex @@ -1,6 +1,8 @@ defmodule ZoeyscomputerWeb.GistLive.FormComponent do use ZoeyscomputerWeb, :live_component + import ZoeyscomputerWeb.GistLive.Select + alias Zoeyscomputer.Gists @impl true @@ -11,7 +13,6 @@ defmodule ZoeyscomputerWeb.GistLive.FormComponent do <%= @title %> <:subtitle>Use this form to manage gist records in your database. - <.simple_form for={@form} id="gist-form" @@ -19,8 +20,22 @@ defmodule ZoeyscomputerWeb.GistLive.FormComponent do phx-change="validate" phx-submit="save" > - <.input field={@form[:code]} type="text" label="Code" /> - <.input field={@form[:lang]} type="text" label="Lang" /> + <.input field={@form[:title]} type="text" label="Title" /> + <.input field={@form[:desc]} type="textarea" label="Description" /> + <.input field={@form[:code]} type="textarea" label="Code" /> + <.select + id="lang-select" + form={@form} + field={:lang} + label="Language" + options={[ + {"JavaScript", "javascript"}, + {"Python", "python"}, + {"Elixir", "elixir"}, + {"Ruby", "ruby"}, + {"Rust", "rust"} + ]} + /> <:actions> <.button phx-disable-with="Saving...">Save Gist @@ -49,6 +64,14 @@ defmodule ZoeyscomputerWeb.GistLive.FormComponent do save_gist(socket, socket.assigns.action, gist_params) end + # Updated to use lang instead of language + def handle_event("change", %{"value" => value}, socket) do + current_params = socket.assigns.form.params || %{} + updated_params = Map.put(current_params, "lang", value) + changeset = Gists.change_gist(socket.assigns.gist, updated_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + defp save_gist(socket, :edit, gist_params) do case Gists.update_gist(socket.assigns.gist, gist_params) do {:ok, gist} -> diff --git a/lib/zoeyscomputer_web/live/gist_live/index.ex b/lib/zoeyscomputer_web/live/gist_live/index.ex index 607c8d4..b6d56fa 100644 --- a/lib/zoeyscomputer_web/live/gist_live/index.ex +++ b/lib/zoeyscomputer_web/live/gist_live/index.ex @@ -11,7 +11,8 @@ defmodule ZoeyscomputerWeb.GistLive.Index do @impl true def handle_params(params, _url, socket) do - {:noreply, apply_action(socket, socket.assigns.live_action, params)} + live_action = socket.assigns.live_action || :index + {:noreply, apply_action(socket, live_action, params)} end defp apply_action(socket, :edit, %{"id" => id}) do @@ -23,7 +24,16 @@ defmodule ZoeyscomputerWeb.GistLive.Index do defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Gist") - |> assign(:gist, %Gist{}) + |> assign(:gist, %Gist{ + code: "", + lang: nil + }) + end + + defp apply_action(socket, :show, %{"id" => id}) do + socket + |> assign(:page_title, "Show Gist") + |> assign(:gist, Gists.get_gist!(id)) end defp apply_action(socket, :index, _params) do @@ -32,6 +42,13 @@ defmodule ZoeyscomputerWeb.GistLive.Index do |> assign(:gist, nil) end + # Catch-all clause for when live_action is nil + defp apply_action(socket, nil, _params) do + socket + |> assign(:page_title, "Listing Gists") + |> assign(:gist, nil) + end + @impl true def handle_info({ZoeyscomputerWeb.GistLive.FormComponent, {:saved, gist}}, socket) do {:noreply, stream_insert(socket, :gists, gist)} diff --git a/lib/zoeyscomputer_web/live/gist_live/index.html.heex b/lib/zoeyscomputer_web/live/gist_live/index.html.heex index 88b300b..95a2599 100644 --- a/lib/zoeyscomputer_web/live/gist_live/index.html.heex +++ b/lib/zoeyscomputer_web/live/gist_live/index.html.heex @@ -12,7 +12,9 @@ rows={@streams.gists} row_click={fn {_id, gist} -> JS.navigate(~p"/gists/#{gist}") end} > - <:col :let={{_id, gist}} label="Code"><%= gist.code %> + <:col :let={{_id, gist}} label="Code"> +

<%= gist.title || gist.code %>

+ <:col :let={{_id, gist}} label="Lang"><%= gist.lang %> <:action :let={{_id, gist}}>
diff --git a/lib/zoeyscomputer_web/live/gist_live/languages.ex b/lib/zoeyscomputer_web/live/gist_live/languages.ex new file mode 100644 index 0000000..e309eca --- /dev/null +++ b/lib/zoeyscomputer_web/live/gist_live/languages.ex @@ -0,0 +1,15 @@ +defmodule ZoeyscomputerWeb.GistLive.Languages do + @languages [ + %{name: "JavaScript", val: "javascript"}, + %{name: "TypeScript", val: "typescript"}, + %{name: "Rust", val: "rust"}, + %{name: "Elixir", val: "elixir"} + ] + + def search_languages(name) do + @languages + |> Enum.filter(fn language -> + String.contains?(String.downcase(language.name), String.downcase(name)) + end) + end +end diff --git a/lib/zoeyscomputer_web/live/gist_live/og_image.ex b/lib/zoeyscomputer_web/live/gist_live/og_image.ex new file mode 100644 index 0000000..4a74a00 --- /dev/null +++ b/lib/zoeyscomputer_web/live/gist_live/og_image.ex @@ -0,0 +1,243 @@ +defmodule ZoeyscomputerWeb.GistLive.OgImage do + @moduledoc """ + Generates OpenGraph preview images for Gists using SVG with syntax highlighting. + """ + require Logger + + @external_resource "priv/static/fonts/WOFF2/Iosevka-Bold.woff2" + @external_resource "priv/static/fonts/WOFF2/Iosevka-Regular.woff2" + @font_bold File.read!("priv/static/fonts/WOFF2/Iosevka-Bold.woff2") + @font_regular File.read!("priv/static/fonts/WOFF2/Iosevka-Regular.woff2") + + @line_height 36 + @code_start_y 210 + @code_start_x 80 + @font_size 24 + @char_width 12 + + defp font_face_styles do + """ + @font-face { + font-family: 'Iosevka'; + src: url(data:font/woff2;charset=utf-8;base64,#{Base.encode64(@font_regular)}) format('woff2'); + font-weight: 400; + font-style: normal; + } + + @font-face { + font-family: 'Iosevka'; + src: url(data:font/woff2;charset=utf-8;base64,#{Base.encode64(@font_bold)}) format('woff2'); + font-weight: 700; + font-style: normal; + } + """ + end + + def generate_preview(gist) do + code_lines = + gist.code + |> preview_code() + |> highlight_code(gist.lang) + |> render_code_lines() + + """ + + + + + + + + + + + + #{escape_text(gist.title || "Gist: #{gist.id}")} + + + + #{escape_text(gist.desc || "No description")} + + + + + + + + #{code_lines} + + + """ + end + + defp render_code_lines(highlighted_html) do + highlighted_html + |> String.split("\n") + |> Enum.with_index() + |> Enum.map_join("\n", fn {line, index} -> + y_position = @code_start_y + index * @line_height + render_line(String.trim(line), y_position) + end) + end + + defp render_line("", _y_position), do: "" + + defp render_line(line, y_position) do + spans = parse_spans(line) + + spans + |> Enum.reduce({[], 0}, fn {content, class}, {spans, x_offset} -> + span = render_span(content, class, x_offset, y_position) + {[span | spans], x_offset + String.length(content)} + end) + |> elem(0) + |> Enum.reverse() + |> Enum.join("\n") + end + + defp parse_spans(""), do: [] + + defp parse_spans(line) do + # Handle non-span text first + line + |> String.split(~r{(|)}, include_captures: true) + |> Enum.reduce({[], nil}, fn + # Opening span tag + <<">, {acc, nil} -> + class = rest |> String.replace_suffix("\">", "") + {acc, class} + + # Closing span tag + "", {acc, _class} -> + {acc, nil} + + # Content within or outside spans + content, {acc, class} when byte_size(content) > 0 -> + {[{content, class || "default"} | acc], class} + + _, acc_class -> + acc_class + end) + |> elem(0) + |> Enum.reverse() + end + + defp render_span(content, class, x_offset, y_position) when byte_size(content) > 0 do + x = @code_start_x + x_offset * @char_width + "#{escape_text(content)}" + end + + defp render_span(_, _, _, _), do: "" + + defp preview_code(content) do + content + |> String.split("\n") + |> Enum.take(15) + |> Enum.join("\n") + end + + defp escape_text(text) do + text + |> String.replace("&", "&") + |> String.replace("<", "<") + |> String.replace(">", ">") + end + + defp highlight_code(code, language) when is_binary(language) do + lexer = get_lexer_for_language(language) + + String.trim( + code + |> Makeup.highlight_inner_html( + lexer: lexer, + formatter_opts: [ + highlight_tag: "span", + css_class: "makeup", + highlight_css_class: "hll" + ] + ) + |> to_string() + ) + end + + defp highlight_code(code, nil) do + escape_text(code) + end + + defp get_lexer_for_language(language) do + case language do + "elixir" -> Makeup.Lexers.ElixirLexer + "erlang" -> Makeup.Lexers.ErlangLexer + "html" -> Makeup.Lexers.HtmlLexer + "javascript" -> Makeup.Lexers.JsLexer + "python" -> Makeup.Lexers.PythonLexer + "ruby" -> Makeup.Lexers.RubyLexer + "rust" -> Makeup.Lexers.RustLexer + end + end + + def get_webp(gist) do + svg = generate_preview(gist) + tmp_svg_path = Path.join(System.tmp_dir(), "temp_#{:rand.uniform(999_999)}.svg") + + Logger.debug(tmp_svg_path) + + try do + File.write!(tmp_svg_path, svg) + + Logger.debug("wrote svg") + + try do + image = + tmp_svg_path + |> Mogrify.open() + |> Mogrify.custom("define", "svg:include-fonts=true") + |> Mogrify.format("webp") + |> Mogrify.save() + + Logger.debug("wrote image #{image.path}") + + {:ok, image_binary} = File.read(image.path) + + Logger.debug("returning binary") + + {:ok, image_binary} + rescue + e in Mogrify.Error -> + { + :error, + "Conversion failed: #{Exception.message(e)}" + } + end + rescue + e -> {:error, "File operation failed: #{Exception.message(e)}"} + after + File.rm(tmp_svg_path) + end + end +end diff --git a/lib/zoeyscomputer_web/live/gist_live/select.ex b/lib/zoeyscomputer_web/live/gist_live/select.ex new file mode 100644 index 0000000..7701ee9 --- /dev/null +++ b/lib/zoeyscomputer_web/live/gist_live/select.ex @@ -0,0 +1,125 @@ +defmodule ZoeyscomputerWeb.GistLive.Select do + use Phoenix.Component + + import Phoenix.HTML.Form, only: [input_value: 2] + + attr :id, :string, required: true + attr :form, :any, required: true + attr :field, :atom, required: true + attr :options, :list, required: true + attr :label, :string, default: nil + attr :prompt, :string, default: "Select an option..." + attr :class, :string, default: nil + attr :disabled, :boolean, default: false + + def select(assigns) do + selected_value = input_value(assigns.form, assigns.field) + + assigns = assign(assigns, :selected_value, selected_value) + + ~H""" +
"-container"} phx-hook="SelectHook"> + + + + +
"-dropdown"} + phx-update="ignore" + class={ + [ + "absolute z-10 mt-1 w-full rounded-md py-1 shadow-lg", + "bg-ctp-base border-ctp-surface0 border", + # Initially hidden, toggled by JS + "hidden" + ] + } + > +
+ "-search"} + placeholder="Search..." + class={[ + "w-full rounded-md py-1.5 px-3", + "bg-ctp-mantle text-ctp-text placeholder-ctp-overlay0", + "focus:outline-none focus:ring-2 focus:ring-offset-0 focus:ring-ctp-lavender", + "transition-colors duration-200" + ]} + /> +
+ +
    "-options"} role="listbox" tabindex="-1"> + <%= for {label, value} <- @options do %> +
  • + <%= label %> + <%= if to_string(value) == to_string(@selected_value) do %> + + + + + + <% end %> +
  • + <% end %> +
+
+ + +
+ """ + end + + defp selected_option(_options, value) when is_nil(value), do: nil + + defp selected_option(options, value) do + case Enum.find(options, fn {_label, val} -> to_string(val) == to_string(value) end) do + {label, _value} -> label + _ -> nil + end + end + + defp input_id(%{id: id}, field) when is_atom(field), do: "#{id}_#{field}" + defp input_name(%{name: name}, field), do: "#{name}[#{field}]" +end diff --git a/lib/zoeyscomputer_web/live/gist_live/show.ex b/lib/zoeyscomputer_web/live/gist_live/show.ex index 6013783..836264e 100644 --- a/lib/zoeyscomputer_web/live/gist_live/show.ex +++ b/lib/zoeyscomputer_web/live/gist_live/show.ex @@ -1,5 +1,6 @@ defmodule ZoeyscomputerWeb.GistLive.Show do use ZoeyscomputerWeb, :live_view + import ZoeyscomputerWeb.CodeBlock alias Zoeyscomputer.Gists diff --git a/lib/zoeyscomputer_web/live/gist_live/show.html.heex b/lib/zoeyscomputer_web/live/gist_live/show.html.heex index 5a428fd..6dc06e6 100644 --- a/lib/zoeyscomputer_web/live/gist_live/show.html.heex +++ b/lib/zoeyscomputer_web/live/gist_live/show.html.heex @@ -1,5 +1,5 @@ <.header> - Gist <%= @gist.id %> + <%= @gist.title || "Gist: #{@gist.id}" %> <:subtitle>This is a gist record from your database. <:actions> <.link patch={~p"/gists/#{@gist}/show/edit"} phx-click={JS.push_focus()}> @@ -8,10 +8,21 @@ -<.list> - <:item title="Code"><%= @gist.code %> - <:item title="Lang"><%= @gist.lang %> - + + + + + + + +<.code_block + class="mt-4" + code={@gist.code} + language={@gist.lang} + title={@gist.title} + line_numbers={true} + highlighted_lines={[118, 119, 120]} +/> <.back navigate={~p"/gists"}>Back to gists diff --git a/lib/zoeyscomputer_web/router.ex b/lib/zoeyscomputer_web/router.ex index 51b2023..e320851 100644 --- a/lib/zoeyscomputer_web/router.ex +++ b/lib/zoeyscomputer_web/router.ex @@ -110,6 +110,9 @@ defmodule ZoeyscomputerWeb.Router do live "/users/confirm", UserConfirmationInstructionsLive, :new live "/", HomeLive, :index + get "/gists/:id/preview", GistPreviewController, :show + get "/gists/:id/preview/raw", GistPreviewController, :raw + live "/images/:id", ImageLive.Show, :show live "/gists/:id", GistLive.Show, :show end diff --git a/mix.exs b/mix.exs index adb0120..2c92b67 100644 --- a/mix.exs +++ b/mix.exs @@ -47,6 +47,7 @@ defmodule Zoeyscomputer.MixProject do {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, {:mogrify, "~> 0.9.3"}, + {:phoenix_html_helpers, "~> 1.0"}, {:heroicons, github: "tailwindlabs/heroicons", tag: "v2.1.1", @@ -54,6 +55,12 @@ defmodule Zoeyscomputer.MixProject do app: false, compile: false, depth: 1}, + {:makeup, "~> 1.1"}, + {:makeup_elixir, "~> 0.16"}, + {:makeup_erlang, "~> 0.1"}, + {:makeup_html, "~> 0.1"}, + {:makeup_js, "~> 0.1"}, + {:makeup_rust, "~> 0.3.0"}, {:swoosh, "~> 1.5"}, {:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"}, diff --git a/mix.lock b/mix.lock index cf4aa5c..d8c9fbc 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"}, @@ -25,6 +25,12 @@ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, + "makeup_js": {:hex, :makeup_js, "0.1.0", "ffa8ce9db95d14dcd09045334539d5992d540d63598c592d4805b7674bdd6675", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "3f0c1a5eb52c9737b1679c926574e83bb260ccdedf08b58ee96cca7c685dea75"}, + "makeup_rust": {:hex, :makeup_rust, "0.3.0", "6fc4a6a1508a7fbea35d08f15c19bb3ae3a3d62ffd2aaca7ee4825a8b1204cef", [:mix], [{:makeup, "~> 1.1", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1e79ee1995a3b9df9b7d90af7dc02525989efc41390a52065c23782c94ea94e1"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, @@ -32,11 +38,13 @@ "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, "nanoid": {:hex, :nanoid, "2.1.0", "d192a5bf1d774258bc49762b480fca0e3128178fa6d35a464af2a738526607fd", [:mix], [], "hexpm", "ebc7a342d02d213534a7f93a091d569b9fea7f26fcd3a638dc655060fc1f76ac"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"}, @@ -45,6 +53,7 @@ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, + "rsvg": {:git, "https://github.com/teamon/rsvg.git", "fa9505fa332e9d5da15f80c08f616a4136079735", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "swoosh": {:hex, :swoosh, "1.17.2", "73611f08fc7cb9fa15f4909db36eeb12b70727d5c8b6a7fa0d4a31c6575db29e", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de914359f0ddc134dc0d7735e28922d49d0503f31e4bd66b44e26039c2226d39"}, diff --git a/priv/repo/migrations/20241026212840_update_gists_code_type.exs b/priv/repo/migrations/20241026212840_update_gists_code_type.exs new file mode 100644 index 0000000..2296547 --- /dev/null +++ b/priv/repo/migrations/20241026212840_update_gists_code_type.exs @@ -0,0 +1,9 @@ +defmodule Zoeyscomputer.Repo.Migrations.UpdateGistsCodeType do + use Ecto.Migration + + def change do + alter table(:gists) do + modify :code, :text + end + end +end diff --git a/priv/repo/migrations/20241026223550_add_gists_info.exs b/priv/repo/migrations/20241026223550_add_gists_info.exs new file mode 100644 index 0000000..f2c3c8a --- /dev/null +++ b/priv/repo/migrations/20241026223550_add_gists_info.exs @@ -0,0 +1,10 @@ +defmodule Zoeyscomputer.Repo.Migrations.AddGistsInfo do + use Ecto.Migration + + def change do + alter table(:gists) do + add :title, :string + add :desc, :text + end + end +end diff --git a/priv/repo/migrations/20241027003540_change_gists_id.exs b/priv/repo/migrations/20241027003540_change_gists_id.exs new file mode 100644 index 0000000..0bb1c89 --- /dev/null +++ b/priv/repo/migrations/20241027003540_change_gists_id.exs @@ -0,0 +1,26 @@ +defmodule Zoeyscomputer.Repo.Migrations.ChangeGistsId do + alias Ecto.Repo + alias Zoeyscomputer.IdGenerator + use Ecto.Migration + + import Ecto.Query, only: [from: 2] + + def change do + alter table(:gists) do + add(:new_id, :string) + end + + flush() + + execute """ + UPDATE gists SET new_id = substring(md5(random()::text), 0, 8) + """ + + alter table(:gists) do + remove(:id) + modify(:new_id, :string, primary_key: true) + end + + rename(table(:gists), :new_id, to: :id) + end +end diff --git a/priv/repo/migrations/20241027005245_gists_author_field.exs b/priv/repo/migrations/20241027005245_gists_author_field.exs new file mode 100644 index 0000000..f865814 --- /dev/null +++ b/priv/repo/migrations/20241027005245_gists_author_field.exs @@ -0,0 +1,9 @@ +defmodule Zoeyscomputer.Repo.Migrations.GistsAuthorField do + use Ecto.Migration + + def change do + alter table(:gists) do + add :author_id, references(:users, on_delete: :nothing) + end + end +end diff --git a/priv/static/fonts/Iosevka.css b/priv/static/fonts/Iosevka.css index f9f3062..5c19018 100644 --- a/priv/static/fonts/Iosevka.css +++ b/priv/static/fonts/Iosevka.css @@ -5,7 +5,7 @@ font-weight: 100; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Thin.woff2') format('woff2'), url('TTF/Iosevka-Thin.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Thin.woff2') format('woff2'), url('TTF/Iosevka-Thin.ttf') format('truetype'); } @font-face { @@ -14,7 +14,7 @@ font-weight: 100; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedThin.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThin.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedThin.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThin.ttf') format('truetype'); } @font-face { @@ -23,7 +23,7 @@ font-weight: 100; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype'); } @font-face { @@ -31,7 +31,7 @@ font-display: swap; font-weight: 100; font-stretch: normal; - src: url('WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ThinOblique.ttf') format('truetype'); } @font-face { @@ -40,7 +40,7 @@ font-weight: 100; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype'); } @font-face { @@ -48,7 +48,7 @@ font-display: swap; font-weight: 100; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedThinOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinOblique.ttf') format('truetype'); } @font-face { @@ -57,7 +57,7 @@ font-weight: 100; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-ThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ThinItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ThinItalic.ttf') format('truetype'); } @font-face { @@ -66,7 +66,7 @@ font-weight: 100; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedThinItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedThinItalic.ttf') format('truetype'); } @font-face { @@ -75,7 +75,7 @@ font-weight: 200; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-ExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtraLight.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtraLight.ttf') format('truetype'); } @font-face { @@ -84,7 +84,7 @@ font-weight: 200; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLight.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLight.ttf') format('truetype'); } @font-face { @@ -93,7 +93,7 @@ font-weight: 200; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype'); } @font-face { @@ -101,7 +101,7 @@ font-display: swap; font-weight: 200; font-stretch: normal; - src: url('WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightOblique.ttf') format('truetype'); } @font-face { @@ -110,7 +110,7 @@ font-weight: 200; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype'); } @font-face { @@ -118,7 +118,7 @@ font-display: swap; font-weight: 200; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightOblique.ttf') format('truetype'); } @font-face { @@ -127,7 +127,7 @@ font-weight: 200; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-ExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraLightItalic.ttf') format('truetype'); } @font-face { @@ -136,7 +136,7 @@ font-weight: 200; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraLightItalic.ttf') format('truetype'); } @font-face { @@ -145,7 +145,7 @@ font-weight: 300; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Light.woff2') format('woff2'), url('TTF/Iosevka-Light.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Light.woff2') format('woff2'), url('TTF/Iosevka-Light.ttf') format('truetype'); } @font-face { @@ -154,7 +154,7 @@ font-weight: 300; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLight.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedLight.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLight.ttf') format('truetype'); } @font-face { @@ -163,7 +163,7 @@ font-weight: 300; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype'); } @font-face { @@ -171,7 +171,7 @@ font-display: swap; font-weight: 300; font-stretch: normal; - src: url('WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-LightOblique.woff2') format('woff2'), url('TTF/Iosevka-LightOblique.ttf') format('truetype'); } @font-face { @@ -180,7 +180,7 @@ font-weight: 300; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype'); } @font-face { @@ -188,7 +188,7 @@ font-display: swap; font-weight: 300; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedLightOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightOblique.ttf') format('truetype'); } @font-face { @@ -197,7 +197,7 @@ font-weight: 300; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-LightItalic.woff2') format('woff2'), url('TTF/Iosevka-LightItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-LightItalic.woff2') format('woff2'), url('TTF/Iosevka-LightItalic.ttf') format('truetype'); } @font-face { @@ -206,7 +206,7 @@ font-weight: 300; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedLightItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedLightItalic.ttf') format('truetype'); } @font-face { @@ -215,7 +215,7 @@ font-weight: 400; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Regular.woff2') format('woff2'), url('TTF/Iosevka-Regular.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Regular.woff2') format('woff2'), url('TTF/Iosevka-Regular.ttf') format('truetype'); } @font-face { @@ -224,7 +224,7 @@ font-weight: 400; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-Extended.woff2') format('woff2'), url('TTF/Iosevka-Extended.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Extended.woff2') format('woff2'), url('TTF/Iosevka-Extended.ttf') format('truetype'); } @font-face { @@ -233,7 +233,7 @@ font-weight: 400; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype'); } @font-face { @@ -241,7 +241,7 @@ font-display: swap; font-weight: 400; font-stretch: normal; - src: url('WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Oblique.woff2') format('woff2'), url('TTF/Iosevka-Oblique.ttf') format('truetype'); } @font-face { @@ -250,7 +250,7 @@ font-weight: 400; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype'); } @font-face { @@ -258,7 +258,7 @@ font-display: swap; font-weight: 400; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedOblique.ttf') format('truetype'); } @font-face { @@ -267,7 +267,7 @@ font-weight: 400; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-Italic.woff2') format('woff2'), url('TTF/Iosevka-Italic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Italic.woff2') format('woff2'), url('TTF/Iosevka-Italic.ttf') format('truetype'); } @font-face { @@ -276,7 +276,7 @@ font-weight: 400; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedItalic.ttf') format('truetype'); } @font-face { @@ -285,7 +285,7 @@ font-weight: 500; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Medium.woff2') format('woff2'), url('TTF/Iosevka-Medium.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Medium.woff2') format('woff2'), url('TTF/Iosevka-Medium.ttf') format('truetype'); } @font-face { @@ -294,7 +294,7 @@ font-weight: 500; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedMedium.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMedium.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedMedium.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMedium.ttf') format('truetype'); } @font-face { @@ -303,7 +303,7 @@ font-weight: 500; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype'); } @font-face { @@ -311,7 +311,7 @@ font-display: swap; font-weight: 500; font-stretch: normal; - src: url('WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-MediumOblique.woff2') format('woff2'), url('TTF/Iosevka-MediumOblique.ttf') format('truetype'); } @font-face { @@ -320,7 +320,7 @@ font-weight: 500; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype'); } @font-face { @@ -328,7 +328,7 @@ font-display: swap; font-weight: 500; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedMediumOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumOblique.ttf') format('truetype'); } @font-face { @@ -337,7 +337,7 @@ font-weight: 500; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-MediumItalic.woff2') format('woff2'), url('TTF/Iosevka-MediumItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-MediumItalic.woff2') format('woff2'), url('TTF/Iosevka-MediumItalic.ttf') format('truetype'); } @font-face { @@ -346,7 +346,7 @@ font-weight: 500; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedMediumItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedMediumItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedMediumItalic.ttf') format('truetype'); } @font-face { @@ -355,7 +355,7 @@ font-weight: 600; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-SemiBold.woff2') format('woff2'), url('TTF/Iosevka-SemiBold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-SemiBold.woff2') format('woff2'), url('TTF/Iosevka-SemiBold.ttf') format('truetype'); } @font-face { @@ -364,7 +364,7 @@ font-weight: 600; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedSemiBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedSemiBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBold.ttf') format('truetype'); } @font-face { @@ -373,7 +373,7 @@ font-weight: 600; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype'); } @font-face { @@ -381,7 +381,7 @@ font-display: swap; font-weight: 600; font-stretch: normal; - src: url('WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-SemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldOblique.ttf') format('truetype'); } @font-face { @@ -390,7 +390,7 @@ font-weight: 600; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype'); } @font-face { @@ -398,7 +398,7 @@ font-display: swap; font-weight: 600; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedSemiBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldOblique.ttf') format('truetype'); } @font-face { @@ -407,7 +407,7 @@ font-weight: 600; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-SemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-SemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-SemiBoldItalic.ttf') format('truetype'); } @font-face { @@ -416,7 +416,7 @@ font-weight: 600; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedSemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedSemiBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedSemiBoldItalic.ttf') format('truetype'); } @font-face { @@ -425,7 +425,7 @@ font-weight: 700; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Bold.woff2') format('woff2'), url('TTF/Iosevka-Bold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Bold.woff2') format('woff2'), url('TTF/Iosevka-Bold.ttf') format('truetype'); } @font-face { @@ -434,7 +434,7 @@ font-weight: 700; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBold.ttf') format('truetype'); } @font-face { @@ -443,7 +443,7 @@ font-weight: 700; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype'); } @font-face { @@ -451,7 +451,7 @@ font-display: swap; font-weight: 700; font-stretch: normal; - src: url('WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-BoldOblique.woff2') format('woff2'), url('TTF/Iosevka-BoldOblique.ttf') format('truetype'); } @font-face { @@ -460,7 +460,7 @@ font-weight: 700; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype'); } @font-face { @@ -468,7 +468,7 @@ font-display: swap; font-weight: 700; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldOblique.ttf') format('truetype'); } @font-face { @@ -477,7 +477,7 @@ font-weight: 700; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-BoldItalic.woff2') format('woff2'), url('TTF/Iosevka-BoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-BoldItalic.woff2') format('woff2'), url('TTF/Iosevka-BoldItalic.ttf') format('truetype'); } @font-face { @@ -486,7 +486,7 @@ font-weight: 700; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedBoldItalic.ttf') format('truetype'); } @font-face { @@ -495,7 +495,7 @@ font-weight: 800; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-ExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtraBold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtraBold.ttf') format('truetype'); } @font-face { @@ -504,7 +504,7 @@ font-weight: 800; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBold.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraBold.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBold.ttf') format('truetype'); } @font-face { @@ -513,7 +513,7 @@ font-weight: 800; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype'); } @font-face { @@ -521,7 +521,7 @@ font-display: swap; font-weight: 800; font-stretch: normal; - src: url('WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldOblique.ttf') format('truetype'); } @font-face { @@ -530,7 +530,7 @@ font-weight: 800; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype'); } @font-face { @@ -538,7 +538,7 @@ font-display: swap; font-weight: 800; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraBoldOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldOblique.ttf') format('truetype'); } @font-face { @@ -547,7 +547,7 @@ font-weight: 800; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-ExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtraBoldItalic.ttf') format('truetype'); } @font-face { @@ -556,7 +556,7 @@ font-weight: 800; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedExtraBoldItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedExtraBoldItalic.ttf') format('truetype'); } @font-face { @@ -565,7 +565,7 @@ font-weight: 900; font-stretch: normal; font-style: normal; - src: url('WOFF2/Iosevka-Heavy.woff2') format('woff2'), url('TTF/Iosevka-Heavy.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-Heavy.woff2') format('woff2'), url('TTF/Iosevka-Heavy.ttf') format('truetype'); } @font-face { @@ -574,7 +574,7 @@ font-weight: 900; font-stretch: expanded; font-style: normal; - src: url('WOFF2/Iosevka-ExtendedHeavy.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavy.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedHeavy.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavy.ttf') format('truetype'); } @font-face { @@ -583,7 +583,7 @@ font-weight: 900; font-stretch: normal; font-style: oblique; - src: url('WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype'); } @font-face { @@ -591,7 +591,7 @@ font-display: swap; font-weight: 900; font-stretch: normal; - src: url('WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-HeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-HeavyOblique.ttf') format('truetype'); } @font-face { @@ -600,7 +600,7 @@ font-weight: 900; font-stretch: expanded; font-style: oblique; - src: url('WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype'); } @font-face { @@ -608,7 +608,7 @@ font-display: swap; font-weight: 900; font-stretch: expanded; - src: url('WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedHeavyOblique.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyOblique.ttf') format('truetype'); } @font-face { @@ -617,7 +617,7 @@ font-weight: 900; font-stretch: normal; font-style: italic; - src: url('WOFF2/Iosevka-HeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-HeavyItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-HeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-HeavyItalic.ttf') format('truetype'); } @font-face { @@ -626,5 +626,5 @@ font-weight: 900; font-stretch: expanded; font-style: italic; - src: url('WOFF2/Iosevka-ExtendedHeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyItalic.ttf') format('truetype'); + src: url('/fonts/WOFF2/Iosevka-ExtendedHeavyItalic.woff2') format('woff2'), url('TTF/Iosevka-ExtendedHeavyItalic.ttf') format('truetype'); }