layered-assistant-rails 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/NOTICE +7 -0
  3. data/README.md +1 -1
  4. data/app/controllers/layered/assistant/assistants_controller.rb +1 -0
  5. data/app/controllers/layered/assistant/conversations_controller.rb +1 -0
  6. data/app/controllers/layered/assistant/panel/conversations_controller.rb +1 -0
  7. data/app/controllers/layered/assistant/providers_controller.rb +1 -0
  8. data/app/controllers/layered/assistant/public/conversations_controller.rb +1 -0
  9. data/app/helpers/layered/assistant/messages_helper.rb +55 -3
  10. data/app/helpers/layered/assistant/panel_helper.rb +2 -2
  11. data/app/javascript/layered_assistant/message_streaming.js +63 -108
  12. data/app/models/layered/assistant/conversation.rb +18 -0
  13. data/app/models/layered/assistant/message.rb +17 -3
  14. data/app/services/layered/assistant/chunk_service.rb +17 -1
  15. data/app/views/layered/assistant/conversations/show.html.erb +2 -2
  16. data/app/views/layered/assistant/panel/conversations/_header.html.erb +1 -1
  17. data/app/views/layered/assistant/panel/conversations/show.html.erb +1 -1
  18. data/app/views/layered/assistant/public/conversations/show.html.erb +2 -2
  19. data/app/views/layered/assistant/public/panel/conversations/_header.html.erb +1 -1
  20. data/app/views/layered/assistant/public/panel/conversations/show.html.erb +1 -1
  21. data/app/views/layered/assistant/setup/_setup.html.erb +5 -0
  22. data/config/importmap.rb +0 -1
  23. data/db/migrate/20260312000000_create_layered_assistant_tables.rb +1 -1
  24. data/db/migrate/20260315000000_add_stopped_to_layered_assistant_messages.rb +1 -1
  25. data/db/migrate/20260315100000_add_response_timing_to_layered_assistant_messages.rb +1 -1
  26. data/db/migrate/20260317000000_normalise_provider_protocol_values.rb +1 -1
  27. data/lib/generators/layered/assistant/install_generator.rb +6 -6
  28. data/lib/generators/layered/assistant/migrations_generator.rb +4 -4
  29. data/lib/layered/assistant/version.rb +1 -1
  30. metadata +3 -3
  31. data/app/javascript/layered_assistant/vendor/marked.esm.js +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73d76e0d5823dd142739589446af268fa9fd692761b135346b976a5f40f7135a
4
- data.tar.gz: b04b43944799242728c61f84f44dbe0e51eb0a28979e657658898134210a582b
3
+ metadata.gz: b0c052e155f7489ab469b2fe1cc4a59b9285fceaaf2595c5947287a95f31a1ed
4
+ data.tar.gz: 8449a3ab76b185cae858779e37fe6888679a1d9f96b76717871acf3fa5b49443
5
5
  SHA512:
6
- metadata.gz: 2b70471491319dadc935a64aff27392af05b882720884bca57991b45bc583f7e389d375baaa463a7c416f196a95d52dca5da21d03e01313441618383b762a1b4
7
- data.tar.gz: f52e344cfb64db8788515b07245615017b50c15da0411d1af76428bd3d08e2d0e5b7c8a828f8dc02ab838a14f6b7872eb87aeb694ea7e2492989de84f691753e
6
+ metadata.gz: 614f4ec4a6e1a592604a9f98572a61cd39808bd47dbcc9e2fcc8d80532dedb378f708877770aef11aaed25f9838be1a867b844b170efbdcb7be6a30dadb7d8b5
7
+ data.tar.gz: 2655b683321d5e9a191106d8e20ab887b679d1b524e504a165a399dd30ef94874078908112ae6481c6bcbc7f2e00df07b9c3ea842e05313c7b9f3bd57b4eff74
data/NOTICE ADDED
@@ -0,0 +1,7 @@
1
+ layered-assistant-rails
2
+
3
+ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830).
4
+
5
+ This product includes software developed by LAYERED AI LIMITED and contributors.
6
+
7
+ Licensed under the Apache License, Version 2.0.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # layered-assistant-rails
2
2
 
3
- [![CI](https://github.com/layered-ai-public/layered-ui-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/layered-ai-public/layered-assistant-rails/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/layered-ai-public/layered-assistant-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/layered-ai-public/layered-assistant-rails/actions/workflows/ci.yml)
4
4
  [![WCAG 2.2 AA](https://img.shields.io/badge/WCAG_2.2-AA-green)](https://www.w3.org/WAI/WCAG22/quickref/)
5
5
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
6
  [![Website](https://img.shields.io/badge/Website-layered.ai-purple)](https://www.layered.ai/)
@@ -16,6 +16,7 @@ module Layered
16
16
 
17
17
  def create
18
18
  @assistant = Assistant.new(assistant_params)
19
+ @assistant.owner = l_ui_current_user
19
20
 
20
21
  if @assistant.save
21
22
  redirect_to layered_assistant.assistants_path, notice: "Assistant was successfully created."
@@ -32,6 +32,7 @@ module Layered
32
32
  def create
33
33
  @conversation = Conversation.new(conversation_params)
34
34
  @conversation.owner = l_ui_current_user
35
+ @conversation.assistant = scoped(Assistant).find(conversation_params[:assistant_id]) if conversation_params[:assistant_id].present?
35
36
  @conversation.name = Conversation.default_name if @conversation.name.blank?
36
37
  if @conversation.save
37
38
  redirect_to layered_assistant.conversation_path(@conversation), notice: "Conversation was successfully created."
@@ -25,6 +25,7 @@ module Layered
25
25
  def create
26
26
  @conversation = Conversation.new(conversation_params)
27
27
  @conversation.owner = l_ui_current_user
28
+ @conversation.assistant = scoped(Assistant).find(conversation_params[:assistant_id]) if conversation_params[:assistant_id].present?
28
29
  @conversation.name = Conversation.default_name if @conversation.name.blank?
29
30
 
30
31
  if @conversation.save
@@ -15,6 +15,7 @@ module Layered
15
15
 
16
16
  def create
17
17
  @provider = Provider.new(provider_params)
18
+ @provider.owner = l_ui_current_user
18
19
 
19
20
  if @provider.save
20
21
  Models::CreateService.new(@provider).call if params[:provider][:create_models] == "1"
@@ -19,6 +19,7 @@ module Layered
19
19
  end
20
20
 
21
21
  def show
22
+ @page_title = @conversation.name
22
23
  @messages = @conversation.messages.includes(:model).by_created_at
23
24
  @conversations = if session_conversation_uids.any?
24
25
  Conversation.joins(:assistant).merge(Assistant.publicly_available)
@@ -35,8 +35,31 @@ module Layered
35
35
  def render_message_content(message)
36
36
  return if message.content.blank?
37
37
 
38
+ render_markdown(message.content)
39
+ end
40
+
41
+ # Renders accumulated content for streaming, holding back any
42
+ # trailing unclosed code fence that Kramdown can't parse correctly.
43
+ # Returns { html:, has_unclosed_fence: } so the caller can decide
44
+ # whether to show a typing indicator.
45
+ def render_streaming_markdown(content)
46
+ return { html: "", has_unclosed_fence: false } if content.blank?
47
+
48
+ safe = strip_unclosed_fence(content)
49
+ {
50
+ html: safe.present? ? render_markdown(safe) : "",
51
+ has_unclosed_fence: safe.length < content.length
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ def render_markdown(content)
58
+ markdown = unwrap_markdown_fence(content)
59
+ markdown = ensure_blank_line_before_tables(markdown)
60
+
38
61
  html = Kramdown::Document.new(
39
- unwrap_markdown_fence(message.content),
62
+ markdown,
40
63
  input: "GFM",
41
64
  syntax_highlighter: nil
42
65
  ).to_html
@@ -44,8 +67,8 @@ module Layered
44
67
  sanitize(html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
45
68
  end
46
69
 
47
- private
48
-
70
+ # Some LLMs wrap their entire response in a ```markdown fence.
71
+ # Strip it so Kramdown processes the inner content directly.
49
72
  def unwrap_markdown_fence(content)
50
73
  if content.start_with?("```markdown\n") && content.end_with?("\n```")
51
74
  content.delete_prefix("```markdown\n").delete_suffix("\n```")
@@ -53,6 +76,35 @@ module Layered
53
76
  content
54
77
  end
55
78
  end
79
+
80
+ # Kramdown GFM requires a blank line before a table, but LLMs often
81
+ # place tables directly after headings or paragraphs. Insert one
82
+ # where missing so that Kramdown recognises the table syntax.
83
+ def ensure_blank_line_before_tables(text)
84
+ text.gsub(/([^\n])\n(\|[^\n]+\|\s*\n\|[\s:|-]+\|\s*\n)/, "\\1\n\n\\2")
85
+ end
86
+
87
+ # Returns text up to (but not including) any trailing unclosed code
88
+ # fence. Kramdown can't produce valid output for a half-open fence,
89
+ # so we hold it back until the closing marker arrives.
90
+ def strip_unclosed_fence(text)
91
+ fence_marker = nil
92
+ fence_start = 0
93
+ pos = 0
94
+
95
+ text.each_line do |line|
96
+ trimmed = line.lstrip
97
+ if fence_marker.nil? && (match = trimmed.match(/\A(`{3,}|~{3,})/))
98
+ fence_marker = match[1]
99
+ fence_start = pos
100
+ elsif fence_marker && trimmed.match?(/\A#{Regexp.escape(fence_marker[0])}{#{fence_marker.length},}\s*\z/)
101
+ fence_marker = nil
102
+ end
103
+ pos += line.length
104
+ end
105
+
106
+ fence_marker ? text[0, fence_start] : text
107
+ end
56
108
  end
57
109
  end
58
110
  end
@@ -18,8 +18,8 @@ module Layered
18
18
  # Any extra keyword arguments are forwarded to the respective
19
19
  # +turbo_frame_tag+ call as HTML attributes.
20
20
 
21
- def layered_assistant_panel_header(**options)
22
- turbo_frame_tag "assistant_panel_header", **options
21
+ def layered_assistant_panel_header(**options, &block)
22
+ turbo_frame_tag "assistant_panel_header", **options, &block
23
23
  end
24
24
 
25
25
  def layered_assistant_panel_body(**options)
@@ -1,108 +1,45 @@
1
- import "@hotwired/turbo-rails"
2
- import { marked } from "marked"
3
-
4
- // Configure marked for GFM (matching server-side Kramdown GFM input)
5
- marked.use({
6
- gfm: true,
7
- breaks: false,
8
- renderer: {
9
- html: () => "" // Strip raw HTML blocks to match server-side sanitisation
10
- }
11
- })
12
-
13
- // Per-element state stored off-DOM
14
- const renderTimers = new WeakMap()
15
- const rawContent = new WeakMap()
1
+ // Incremental markdown rendering for streamed assistant messages.
2
+ //
3
+ // The server broadcasts pre-rendered HTML via a custom Turbo Stream action
4
+ // (render_content). This module reconciles the DOM block by block -
5
+ // patching unchanged blocks in place so fade-in animations aren't
6
+ // interrupted. Both streaming preview and final output use the same
7
+ // server-side Kramdown pipeline, eliminating parser drift.
16
8
 
17
- // Threshold (in chars) of unsettled content before re-showing the indicator
18
- const UNSETTLED_THRESHOLD = 200
19
-
20
- const TYPING_INDICATOR_HTML =
21
- '<div class="l-ui-typing-indicator" role="status" aria-label="Assistant is typing">' +
22
- '<span class="l-ui-typing-indicator__dot"></span>' +
23
- '<span class="l-ui-typing-indicator__dot"></span>' +
24
- '<span class="l-ui-typing-indicator__dot"></span>' +
25
- '</div>'
9
+ import "@hotwired/turbo-rails"
26
10
 
27
- // Find the boundary between complete markdown blocks (safe to parse) and
28
- // the in-progress tail that is still being streamed. We split at the last
29
- // blank line, but never inside an unclosed code fence.
30
- function findBlockBoundary(text) {
31
- const lines = text.split("\n")
32
- let fenceMarker = null
33
- let boundary = 0
34
- let pos = 0
11
+ const pendingHtml = new WeakMap()
12
+ const pendingRender = new WeakMap()
35
13
 
36
- for (let i = 0; i < lines.length; i++) {
37
- const trimmed = lines[i].trimStart()
38
- if (!fenceMarker && (trimmed.startsWith("```") || trimmed.startsWith("~~~"))) {
39
- fenceMarker = trimmed.slice(0, 3)
40
- } else if (fenceMarker && trimmed.startsWith(fenceMarker) && /^[`~]+\s*$/.test(trimmed)) {
41
- fenceMarker = null
42
- }
14
+ function reconcile(target, html) {
15
+ const temp = document.createElement("div")
16
+ temp.innerHTML = html
43
17
 
44
- pos += lines[i].length + 1
18
+ // Snapshot temp.children since moves will mutate the live HTMLCollection
19
+ const newBlocks = [...temp.children]
45
20
 
46
- if (!fenceMarker && lines[i] === "" && i > 0) {
47
- boundary = pos
48
- }
21
+ // Trim excess blocks from previous render
22
+ while (target.children.length > newBlocks.length) {
23
+ target.lastElementChild.remove()
49
24
  }
50
25
 
51
- return boundary
52
- }
53
-
54
- // Append newly settled markdown blocks to the target element. Only the
55
- // portion between the previous boundary and the current one is parsed and
56
- // inserted, so existing DOM nodes stay intact and there is no flicker.
57
- // Turbo replaces the entire element with server-rendered HTML when the
58
- // stream completes, correcting any incremental rendering artefacts.
59
- function renderMarkdown(target) {
60
- const raw = rawContent.get(target) || ""
61
- if (!raw) return
62
-
63
- const boundary = findBlockBoundary(raw)
64
- // No settled blocks yet - the server-rendered typing indicator is still
65
- // in the DOM, so there's nothing to do until a blank line arrives.
66
- if (boundary === 0) return
67
-
68
- const previousBoundary = parseInt(target.dataset.settledBoundary || "0", 10)
69
-
70
- if (boundary > previousBoundary) {
71
- target.dataset.settledBoundary = boundary
72
-
73
- // Remove typing indicator before appending new content
74
- target.querySelector(".l-ui-typing-indicator")?.remove()
75
-
76
- // Parse and append only the new settled portion
77
- const prevCount = target.children.length
78
- target.insertAdjacentHTML("beforeend", marked.parse(raw.substring(previousBoundary, boundary)))
79
-
80
- // Stagger fade-in on newly appended blocks
81
- for (let i = prevCount; i < target.children.length; i++) {
82
- target.children[i].style.animationDelay = `${(i - prevCount) * 120}ms`
83
- target.children[i].classList.add("l-ui-token-fade")
26
+ // Reconcile block by block - preserves existing DOM nodes (and their
27
+ // running fade-in animations) wherever possible
28
+ for (let i = 0; i < newBlocks.length; i++) {
29
+ if (i < target.children.length && target.children[i].tagName === newBlocks[i].tagName) {
30
+ // Same tag - patch content in place
31
+ if (target.children[i].innerHTML !== newBlocks[i].innerHTML) {
32
+ target.children[i].innerHTML = newBlocks[i].innerHTML
33
+ }
34
+ } else if (i < target.children.length) {
35
+ // Tag changed (e.g. <p> became <table>) - replace node
36
+ target.children[i].replaceWith(newBlocks[i])
37
+ } else {
38
+ // New block - append with fade
39
+ newBlocks[i].classList.add("l-ui-token-fade")
40
+ target.appendChild(newBlocks[i])
84
41
  }
85
42
  }
86
-
87
- // Show typing indicator when there's a large unsettled tail, so the user
88
- // knows content is still arriving (e.g. a long code block before the
89
- // closing fence). The indicator is removed automatically when the boundary
90
- // next advances (above).
91
- const tail = raw.length - boundary
92
- if (tail >= UNSETTLED_THRESHOLD && !target.querySelector(".l-ui-typing-indicator")) {
93
- target.insertAdjacentHTML("beforeend", TYPING_INDICATOR_HTML)
94
- }
95
- }
96
-
97
- function scheduleRender(target) {
98
- const existing = renderTimers.get(target)
99
- if (existing) cancelAnimationFrame(existing)
100
-
101
- const id = requestAnimationFrame(() => {
102
- renderTimers.delete(target)
103
- renderMarkdown(target)
104
- })
105
- renderTimers.set(target, id)
106
43
  }
107
44
 
108
45
  Turbo.StreamActions.enable_composer = function () {
@@ -111,23 +48,41 @@ Turbo.StreamActions.enable_composer = function () {
111
48
  })
112
49
  }
113
50
 
114
- Turbo.StreamActions.append_chunk = function () {
51
+ Turbo.StreamActions.render_content = function () {
115
52
  this.targetElements.forEach((target) => {
116
- const text = this.templateContent.textContent || ""
117
-
118
- // Initialise on first chunk
119
- if (!rawContent.has(target)) {
53
+ if (!target.classList.contains("l-ui-markdown")) {
120
54
  target.classList.add("l-ui-markdown")
121
- rawContent.set(target, "")
122
55
  }
123
56
 
124
- // Accumulate raw markdown off-DOM
125
- rawContent.set(target, rawContent.get(target) + text)
126
-
127
- // Schedule debounced render
128
- scheduleRender(target)
57
+ const html = this.templateContent
58
+
59
+ // Always store the latest HTML so the requestAnimationFrame callback
60
+ // uses the newest version - convert DocumentFragment to HTML string
61
+ const div = document.createElement("div")
62
+ div.append(html.cloneNode(true))
63
+ pendingHtml.set(target, div.innerHTML)
64
+
65
+ if (!pendingRender.has(target)) {
66
+ pendingRender.set(target, requestAnimationFrame(() => {
67
+ pendingRender.delete(target)
68
+ const latestHtml = pendingHtml.get(target)
69
+ if (latestHtml != null) {
70
+ reconcile(target, latestHtml)
71
+ pendingHtml.delete(target)
72
+ }
73
+ }))
74
+ }
129
75
  })
130
76
 
131
- // Notify the composer so it can reset its safety timeout
132
77
  document.dispatchEvent(new CustomEvent("assistant:chunk-received"))
133
78
  }
79
+
80
+ Turbo.StreamActions.update_conversation_name = function () {
81
+ const name = this.getAttribute("name")
82
+ const oldName = this.getAttribute("old-name")
83
+ this.targetElements.forEach((el) => { el.textContent = name })
84
+ if (oldName && document.title.includes(oldName)) {
85
+ const i = document.title.indexOf(oldName)
86
+ document.title = document.title.slice(0, i) + name + document.title.slice(i + oldName.length)
87
+ }
88
+ }
@@ -32,6 +32,10 @@ module Layered
32
32
  "New conversation"
33
33
  end
34
34
 
35
+ def responding?
36
+ messages.where(role: :assistant, stopped: false, output_tokens: nil).exists?
37
+ end
38
+
35
39
  def stop_response!
36
40
  with_lock do
37
41
  message = messages.where(role: :assistant, stopped: false).order(created_at: :desc).first
@@ -62,7 +66,21 @@ module Layered
62
66
  return unless name == self.class.default_name
63
67
  return if content.blank?
64
68
 
69
+ old_name = name
65
70
  update!(name: content.truncate(60))
71
+ broadcast_name_updated(old_name)
72
+ end
73
+
74
+ private
75
+
76
+ def broadcast_name_updated(old_name)
77
+ css_class = "#{ActionView::RecordIdentifier.dom_id(self)}_name"
78
+ Turbo::StreamsChannel.broadcast_action_to(
79
+ self,
80
+ action: :update_conversation_name,
81
+ targets: ".#{css_class}",
82
+ attributes: { name: name, "old-name": old_name }
83
+ )
66
84
  end
67
85
  end
68
86
  end
@@ -45,11 +45,25 @@ module Layered
45
45
  targets: ".#{dom_id(conversation)}_composer"
46
46
  end
47
47
 
48
- def broadcast_chunk(text)
48
+ def broadcast_streaming_content
49
+ rendered = helpers.render_streaming_markdown(content)
50
+ html = rendered[:html]
51
+
52
+ if rendered[:has_unclosed_fence]
53
+ html += helpers.tag.div(
54
+ helpers.tag.span(class: "l-ui-typing-indicator__dot") +
55
+ helpers.tag.span(class: "l-ui-typing-indicator__dot") +
56
+ helpers.tag.span(class: "l-ui-typing-indicator__dot"),
57
+ class: "l-ui-typing-indicator",
58
+ role: "status",
59
+ "aria-label": "Assistant is typing"
60
+ )
61
+ end
62
+
49
63
  broadcast_action_to conversation,
50
- action: :append_chunk,
64
+ action: :render_content,
51
65
  targets: ".#{dom_id(self)}_body",
52
- content: helpers.content_tag(:span, text, class: "l-ui-token-fade")
66
+ content: html
53
67
  end
54
68
 
55
69
  private
@@ -2,6 +2,7 @@ module Layered
2
2
  module Assistant
3
3
  class ChunkService
4
4
  STOP_CHECK_INTERVAL = 25
5
+ BROADCAST_INTERVAL_MS = 25
5
6
 
6
7
  def initialize(message, provider:, started_at: nil)
7
8
  @message = message
@@ -12,6 +13,8 @@ module Layered
12
13
  @output_tokens = 0
13
14
  @chunk_count = 0
14
15
  @stopped = false
16
+ @last_broadcast_at = 0
17
+ @broadcast_pending = false
15
18
  end
16
19
 
17
20
  def mark_started!
@@ -42,17 +45,30 @@ module Layered
42
45
  if text
43
46
  @timer.record_first_token!
44
47
  @message.update!(content: (@message.content || "") + text)
45
- @message.broadcast_chunk(text)
48
+ broadcast_throttled
46
49
  end
47
50
 
48
51
  if @parser.finished?(chunk) || @parser.usage_ready?(chunk)
49
52
  save_token_usage
53
+ @message.broadcast_streaming_content if @broadcast_pending
50
54
  @message.broadcast_updated
51
55
  end
52
56
  end
53
57
 
54
58
  private
55
59
 
60
+ def broadcast_throttled
61
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
62
+ if now - @last_broadcast_at < BROADCAST_INTERVAL_MS
63
+ @broadcast_pending = true
64
+ return
65
+ end
66
+
67
+ @last_broadcast_at = now
68
+ @broadcast_pending = false
69
+ @message.broadcast_streaming_content
70
+ end
71
+
56
72
  def save_token_usage
57
73
  timing = @timer.timing_attrs
58
74
  if @input_tokens == 0 && @output_tokens == 0
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="l-ui-conversation__container">
4
4
  <div class="l-ui-container--spread">
5
- <h1><%= @conversation.name %></h1>
5
+ <h1 class="<%= dom_id(@conversation) %>_name"><%= @conversation.name %></h1>
6
6
  <%= link_to "Back", layered_assistant.conversations_path, class: "l-ui-button--outline" %>
7
7
  </div>
8
8
 
@@ -21,5 +21,5 @@
21
21
  </button>
22
22
  </div>
23
23
 
24
- <%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
24
+ <%= render "layered/assistant/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id, responding: @conversation.responding? %>
25
25
  </div>
@@ -12,7 +12,7 @@
12
12
  <% if assistant_conversations.any? %>
13
13
  <optgroup label="<%= assistant.name %>">
14
14
  <% assistant_conversations.each do |c| %>
15
- <option value="<%= layered_assistant.panel_conversation_path(c) %>" <%= "selected" if selected == layered_assistant.panel_conversation_path(c) %>><%= c.name %></option>
15
+ <option value="<%= layered_assistant.panel_conversation_path(c) %>" <%= "selected" if selected == layered_assistant.panel_conversation_path(c) %> class="<%= dom_id(c) %>_name"><%= c.name %></option>
16
16
  <% end %>
17
17
  </optgroup>
18
18
  <% end %>
@@ -19,6 +19,6 @@
19
19
  </button>
20
20
  </div>
21
21
 
22
- <%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id %>
22
+ <%= render "layered/assistant/panel/messages/composer", conversation: @conversation, models: @models, selected_model_id: @selected_model_id, responding: @conversation.responding? %>
23
23
  </div>
24
24
  <% end %>
@@ -8,7 +8,7 @@
8
8
  data-action="change->conversation-select#navigate">
9
9
  <option value="<%= layered_assistant.public_assistant_path(@conversation.assistant) %>">+ New</option>
10
10
  <% @conversations.each do |c| %>
11
- <option value="<%= layered_assistant.public_conversation_path(c) %>" <%= "selected" if c.id == @conversation.id %>><%= c.name %></option>
11
+ <option value="<%= layered_assistant.public_conversation_path(c) %>" <%= "selected" if c.id == @conversation.id %> class="<%= dom_id(c) %>_name"><%= c.name %></option>
12
12
  <% end %>
13
13
  </select>
14
14
  </div>
@@ -28,5 +28,5 @@
28
28
  </button>
29
29
  </div>
30
30
 
31
- <%= render "layered/assistant/public/messages/composer", conversation: @conversation %>
31
+ <%= render "layered/assistant/public/messages/composer", conversation: @conversation, responding: @conversation.responding? %>
32
32
  </div>
@@ -8,7 +8,7 @@
8
8
  <option value="<%= layered_assistant.public_panel_conversations_path(assistant_id: assistant.id) %>" <%= "selected" if selected == :index %>>Conversations</option>
9
9
  <option value="new" <%= "selected" if selected == :new %>>+ New</option>
10
10
  <% conversations.each do |c| %>
11
- <option value="<%= layered_assistant.public_panel_conversation_path(c) %>" <%= "selected" if selected == c.id %>><%= c.name %></option>
11
+ <option value="<%= layered_assistant.public_panel_conversation_path(c) %>" <%= "selected" if selected == c.id %> class="<%= dom_id(c) %>_name"><%= c.name %></option>
12
12
  <% end %>
13
13
  </select>
14
14
  </div>
@@ -18,6 +18,6 @@
18
18
  </button>
19
19
  </div>
20
20
 
21
- <%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation %>
21
+ <%= render "layered/assistant/public/panel/messages/composer", conversation: @conversation, responding: @conversation.responding? %>
22
22
  </div>
23
23
  <% end %>
@@ -167,6 +167,11 @@ end</code></pre>
167
167
  <td class="l-ui-table__cell"><code>false</code></td>
168
168
  <td class="l-ui-table__cell">Log API errors to stdout from the AI API clients</td>
169
169
  </tr>
170
+ <tr>
171
+ <th class="l-ui-table__cell--primary" scope="row"><code>api_request_timeout</code></th>
172
+ <td class="l-ui-table__cell"><code>210</code></td>
173
+ <td class="l-ui-table__cell">Timeout in seconds for AI API requests</td>
174
+ </tr>
170
175
  <tr>
171
176
  <th class="l-ui-table__cell--primary" scope="row"><code>skip_db_encryption</code></th>
172
177
  <td class="l-ui-table__cell"><code>false</code></td>
data/config/importmap.rb CHANGED
@@ -6,4 +6,3 @@ pin "layered_assistant/messages_controller", to: "layered_assistant/messages_con
6
6
  pin "layered_assistant/panel_controller", to: "layered_assistant/panel_controller.js"
7
7
  pin "layered_assistant/panel_nav_controller", to: "layered_assistant/panel_nav_controller.js"
8
8
  pin "layered_assistant/provider_template_controller", to: "layered_assistant/provider_template_controller.js"
9
- pin "marked", to: "layered_assistant/vendor/marked.esm.js"
@@ -1,4 +1,4 @@
1
- class CreateLayeredAssistantTables < ActiveRecord::Migration[8.1]
1
+ class CreateLayeredAssistantTables < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  create_table :layered_assistant_providers, if_not_exists: true do |t|
4
4
  t.references :owner, polymorphic: true
@@ -1,4 +1,4 @@
1
- class AddStoppedToLayeredAssistantMessages < ActiveRecord::Migration[8.1]
1
+ class AddStoppedToLayeredAssistantMessages < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  add_column :layered_assistant_messages, :stopped, :boolean, default: false, null: false
4
4
  end
@@ -1,4 +1,4 @@
1
- class AddResponseTimingToLayeredAssistantMessages < ActiveRecord::Migration[8.1]
1
+ class AddResponseTimingToLayeredAssistantMessages < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  add_column :layered_assistant_messages, :ttft_ms, :integer
4
4
  add_column :layered_assistant_messages, :response_ms, :integer
@@ -1,4 +1,4 @@
1
- class NormaliseProviderProtocolValues < ActiveRecord::Migration[8.1]
1
+ class NormaliseProviderProtocolValues < ActiveRecord::Migration[8.0]
2
2
  def up
3
3
  execute "UPDATE layered_assistant_providers SET protocol = 'anthropic' WHERE protocol = 'Anthropic'"
4
4
  execute "UPDATE layered_assistant_providers SET protocol = 'openai' WHERE protocol = 'OpenAI'"
@@ -12,8 +12,8 @@ module Layered
12
12
  application_css = "app/assets/tailwind/application.css"
13
13
  application_js = "app/javascript/application.js"
14
14
 
15
- css_ok = File.exist?(application_css) && File.read(application_css).include?('@import "./layered_ui"')
16
- js_ok = File.exist?(application_js) && File.read(application_js).include?('import "layered_ui"')
15
+ css_ok = File.exist?(application_css) && File.read(application_css).match?(%r{@import\s+['"]\.?/?layered_ui['"]})
16
+ js_ok = File.exist?(application_js) && File.read(application_js).match?(%r{import\s+['"]layered_ui['"]})
17
17
 
18
18
  unless css_ok && js_ok
19
19
  say "layered-ui-rails is not installed yet - installing now...", :yellow
@@ -46,10 +46,10 @@ module Layered
46
46
  content = File.read(application_css)
47
47
  import_line = '@import "./layered_assistant";'
48
48
 
49
- return if content.include?(import_line)
49
+ return if content.match?(%r{@import\s+['"]\.?/?layered_assistant['"]})
50
50
 
51
51
  # Insert after the layered_ui import (which must already be present)
52
- inject_into_file application_css, "\n#{import_line}", after: '@import "./layered_ui";'
52
+ inject_into_file application_css, "\n#{import_line}", after: %r{@import\s+['"]\.?/?layered_ui['"];?}
53
53
  say "Added import to #{application_css}", :green
54
54
  end
55
55
 
@@ -61,10 +61,10 @@ module Layered
61
61
  content = File.read(application_js)
62
62
  import_line = 'import "layered_assistant"'
63
63
 
64
- return if content.include?(import_line)
64
+ return if content.match?(%r{import\s+['"]layered_assistant['"]})
65
65
 
66
66
  # Insert after the layered_ui import (which must already be present)
67
- inject_into_file application_js, "\n#{import_line}", after: 'import "layered_ui"'
67
+ inject_into_file application_js, "\n#{import_line}", after: %r{import\s+['"]layered_ui['"];?}
68
68
  say "Added import to #{application_js}", :green
69
69
  end
70
70
 
@@ -6,14 +6,15 @@ module Layered
6
6
 
7
7
  def copy_migrations
8
8
  engine_migrations_path = Layered::Assistant::Engine.root.join("db/migrate")
9
- app_migrations_path = Rails.root.join("db/migrate")
9
+ app_migrations_dir = "db/migrate"
10
10
 
11
11
  unless engine_migrations_path.exist?
12
12
  say "No migrations found in layered-assistant-rails.", :yellow
13
13
  return
14
14
  end
15
15
 
16
- existing_migrations = Dir[app_migrations_path.join("*.rb")].map { |f| migration_name(File.basename(f)) }
16
+ app_migrations_path = File.join(destination_root, app_migrations_dir)
17
+ existing_migrations = Dir[File.join(app_migrations_path, "*.rb")].map { |f| migration_name(File.basename(f)) }
17
18
 
18
19
  timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
19
20
 
@@ -26,10 +27,9 @@ module Layered
26
27
  next
27
28
  end
28
29
 
29
- destination = app_migrations_path.join("#{timestamp}_#{name}.layered_assistant.rb")
30
30
  content = "# This migration comes from layered_assistant (originally #{basename.split("_").first})\n" + File.read(source)
31
31
 
32
- create_file destination, content
32
+ create_file "#{app_migrations_dir}/#{timestamp}_#{name}.layered_assistant.rb", content
33
33
  timestamp += 1
34
34
  end
35
35
  end
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Assistant
3
- VERSION = "0.2.0"
3
+ VERSION = "0.2.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-assistant-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -299,6 +299,7 @@ extra_rdoc_files: []
299
299
  files:
300
300
  - AGENTS.md
301
301
  - LICENSE
302
+ - NOTICE
302
303
  - README.md
303
304
  - Rakefile
304
305
  - app/assets/tailwind/layered/assistant/styles.css
@@ -331,7 +332,6 @@ files:
331
332
  - app/javascript/layered_assistant/panel_controller.js
332
333
  - app/javascript/layered_assistant/panel_nav_controller.js
333
334
  - app/javascript/layered_assistant/provider_template_controller.js
334
- - app/javascript/layered_assistant/vendor/marked.esm.js
335
335
  - app/jobs/layered/assistant/application_job.rb
336
336
  - app/jobs/layered/assistant/messages/response_job.rb
337
337
  - app/models/layered/assistant/application_record.rb
@@ -425,7 +425,7 @@ post_install_message: |
425
425
  • Copy the layered assistant CSS to your host app at app/assets/tailwind/layered_assistant.css
426
426
  • This approach ensures the CSS is processed with your host app's Tailwind configuration
427
427
  • Add an import statement to your app/assets/tailwind/application.css
428
- • Add `import "layered_assistant"` to your app/javascript/application.js (just after `import "@hotwired/turbo-rails"`, if present)
428
+ • Add `import "layered_assistant"` to your app/javascript/application.js (just after `import "layered_ui"`, which must already be present)
429
429
  • Copy engine migrations to your app's db/migrate/
430
430
 
431
431
  If these imports already exist, they will not be duplicated.
@@ -1,72 +0,0 @@
1
- /**
2
- * marked v17.0.3 - a markdown parser
3
- * Copyright (c) 2018-2026, MarkedJS. (MIT License)
4
- * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)
5
- * https://github.com/markedjs/marked
6
- */
7
-
8
- /**
9
- * DO NOT EDIT THIS FILE
10
- * The code in this file is generated from files in ./src/
11
- */
12
-
13
- function M(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=M();function H(u){T=u}var _={exec:()=>null};function k(u,e=""){let t=typeof u=="string"?u:u.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var Re=(()=>{try{return!!new RegExp("(?<=1)(?<!1)")}catch{return!1}})(),m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:u=>new RegExp(`^( {0,3}${u})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}#`),htmlBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}<(?:[a-z].*>|!--)`,"i"),blockquoteBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}>`)},Te=/^(?:[ \t]*(?:\n|$))+/,Oe=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,we=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,I=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ye=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,N=/ {0,3}(?:[*+-]|\d{1,9}[.)])/,re=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,se=k(re).replace(/bull/g,N).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Pe=k(re).replace(/bull/g,N).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Se=/^[^\n]+/,F=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,$e=k(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",F).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_e=k(/^(bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,N).getRegex(),q="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",j=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Le=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",j).replace("tag",q).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ie=k(Q).replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",q).getRegex(),Me=k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",ie).getRegex(),U={blockquote:Me,code:Oe,def:$e,fences:we,heading:ye,hr:I,html:Le,lheading:se,list:_e,newline:Te,paragraph:ie,table:_,text:Se},te=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",q).getRegex(),ze={...U,lheading:Pe,table:te,paragraph:k(Q).replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",te).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",q).getRegex()},Ce={...U,html:k(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",j).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:_,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(Q).replace("hr",I).replace("heading",` *#{1,6} *[^
14
- ]`).replace("lheading",se).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Ae=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Ie=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,oe=/^( {2,}|\\)\n(?!\s*$)/,Ee=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,v=/[\p{P}\p{S}]/u,K=/[\s\p{P}\p{S}]/u,ae=/[^\s\p{P}\p{S}]/u,Be=k(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,K).getRegex(),le=/(?!~)[\p{P}\p{S}]/u,De=/(?!~)[\s\p{P}\p{S}]/u,qe=/(?:[^\s\p{P}\p{S}]|~)/u,ue=/(?![*_])[\p{P}\p{S}]/u,ve=/(?![*_])[\s\p{P}\p{S}]/u,Ge=/(?:[^\s\p{P}\p{S}]|[*_])/u,He=k(/link|precode-code|html/,"g").replace("link",/\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Re?"(?<!`)()":"(^^|[^`])").replace("code",/(?<b>`+)[^`]+\k<b>(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),pe=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,Ze=k(pe,"u").replace(/punct/g,v).getRegex(),Ne=k(pe,"u").replace(/punct/g,le).getRegex(),ce="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=k(ce,"gu").replace(/notPunctSpace/g,ae).replace(/punctSpace/g,K).replace(/punct/g,v).getRegex(),Fe=k(ce,"gu").replace(/notPunctSpace/g,qe).replace(/punctSpace/g,De).replace(/punct/g,le).getRegex(),je=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ae).replace(/punctSpace/g,K).replace(/punct/g,v).getRegex(),Ue=k(/^~~?(?:((?!~)punct)|[^\s~])/,"u").replace(/punct/g,ue).getRegex(),Ke="^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)",We=k(Ke,"gu").replace(/notPunctSpace/g,Ge).replace(/punctSpace/g,ve).replace(/punct/g,ue).getRegex(),Xe=k(/\\(punct)/,"gu").replace(/punct/g,v).getRegex(),Je=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Ve=k(j).replace("(?:-->|$)","-->").getRegex(),Ye=k("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",Ve).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),D=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+[^`]*?`+(?!`)|[^\[\]\\`])*?/,et=k(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",D).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),he=k(/^!?\[(label)\]\[(ref)\]/).replace("label",D).replace("ref",F).getRegex(),ke=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",F).getRegex(),tt=k("reflink|nolink(?!\\()","g").replace("reflink",he).replace("nolink",ke).getRegex(),ne=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,W={_backpedal:_,anyPunctuation:Xe,autolink:Je,blockSkip:He,br:oe,code:Ie,del:_,delLDelim:_,delRDelim:_,emStrongLDelim:Ze,emStrongRDelimAst:Qe,emStrongRDelimUnd:je,escape:Ae,link:et,nolink:ke,punctuation:Be,reflink:he,reflinkSearch:tt,tag:Ye,text:Ee,url:_},nt={...W,link:k(/^!?\[(label)\]\((.*?)\)/).replace("label",D).getRegex(),reflink:k(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",D).getRegex()},Z={...W,emStrongRDelimAst:Fe,emStrongLDelim:Ne,delLDelim:Ue,delRDelim:We,url:k(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",ne).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:k(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/).replace("protocol",ne).getRegex()},rt={...Z,br:k(oe).replace("{2,}","*").getRegex(),text:k(Z.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},E={normal:U,gfm:ze,pedantic:Ce},z={normal:W,gfm:Z,breaks:rt,pedantic:nt};var st={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},de=u=>st[u];function O(u,e){if(e){if(m.escapeTest.test(u))return u.replace(m.escapeReplace,de)}else if(m.escapeTestNoEncode.test(u))return u.replace(m.escapeReplaceNoEncode,de);return u}function X(u){try{u=encodeURI(u).replace(m.percentDecode,"%")}catch{return null}return u}function J(u,e){let t=u.replace(m.findPipe,(i,s,a)=>{let o=!1,l=s;for(;--l>=0&&a[l]==="\\";)o=!o;return o?"|":" |"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(m.slashPipe,"|");return n}function C(u,e,t){let n=u.length;if(n===0)return"";let r=0;for(;r<n;){let i=u.charAt(n-r-1);if(i===e&&!t)r++;else if(i!==e&&t)r++;else break}return u.slice(0,n-r)}function ge(u,e){if(u.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<u.length;n++)if(u[n]==="\\")n++;else if(u[n]===e[0])t++;else if(u[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}function fe(u,e=0){let t=e,n="";for(let r of u)if(r===" "){let i=4-t%4;n+=" ".repeat(i),t+=i}else n+=r,t++;return n}function me(u,e,t,n,r){let i=e.href,s=e.title||null,a=u[1].replace(r.other.outputLinkReplace,"$1");n.state.inLink=!0;let o={type:u[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:s,text:a,tokens:n.inlineTokens(a)};return n.state.inLink=!1,o}function it(u,e,t){let n=u.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(`
15
- `).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[a]=s;return a.length>=r.length?i.slice(r.length):i}).join(`
16
- `)}var w=class{options;rules;lexer;constructor(e){this.options=e||T}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:C(n,`
17
- `)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=it(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=C(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:C(t[0],`
18
- `)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=C(t[0],`
19
- `).split(`
20
- `),r="",i="",s=[];for(;n.length>0;){let a=!1,o=[],l;for(l=0;l<n.length;l++)if(this.rules.other.blockquoteStart.test(n[l]))o.push(n[l]),a=!0;else if(!a)o.push(n[l]);else break;n=n.slice(l);let p=o.join(`
21
- `),c=p.replace(this.rules.other.blockquoteSetextReplace,`
22
- $1`).replace(this.rules.other.blockquoteSetextReplace2,"");r=r?`${r}
23
- ${p}`:p,i=i?`${i}
24
- ${c}`:c;let d=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(c,s,!0),this.lexer.state.top=d,n.length===0)break;let h=s.at(-1);if(h?.type==="code")break;if(h?.type==="blockquote"){let R=h,f=R.raw+`
25
- `+n.join(`
26
- `),S=this.blockquote(f);s[s.length-1]=S,r=r.substring(0,r.length-R.raw.length)+S.raw,i=i.substring(0,i.length-R.text.length)+S.text;break}else if(h?.type==="list"){let R=h,f=R.raw+`
27
- `+n.join(`
28
- `),S=this.list(f);s[s.length-1]=S,r=r.substring(0,r.length-h.raw.length)+S.raw,i=i.substring(0,i.length-R.raw.length)+S.raw,n=f.substring(s.at(-1).raw.length).split(`
29
- `);continue}}return{type:"blockquote",raw:r,tokens:s,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),r=n.length>1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),a=!1;for(;e;){let l=!1,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=fe(t[2].split(`
30
- `,1)[0],t[1].length),h=e.split(`
31
- `,1)[0],R=!d.trim(),f=0;if(this.options.pedantic?(f=2,c=d.trimStart()):R?f=t[1].length+1:(f=d.search(this.rules.other.nonSpaceChar),f=f>4?1:f,c=d.slice(f),f+=t[1].length),R&&this.rules.other.blankLine.test(h)&&(p+=h+`
32
- `,e=e.substring(h.length+1),l=!0),!l){let S=this.rules.other.nextBulletRegex(f),V=this.rules.other.hrRegex(f),Y=this.rules.other.fencesBeginRegex(f),ee=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f),be=this.rules.other.blockquoteBeginRegex(f);for(;e;){let G=e.split(`
33
- `,1)[0],A;if(h=G,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),A=h):A=h.replace(this.rules.other.tabCharGlobal," "),Y.test(h)||ee.test(h)||xe.test(h)||be.test(h)||S.test(h)||V.test(h))break;if(A.search(this.rules.other.nonSpaceChar)>=f||!h.trim())c+=`
34
- `+A.slice(f);else{if(R||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||Y.test(d)||ee.test(d)||V.test(d))break;c+=`
35
- `+h}R=!h.trim(),p+=G+`
36
- `,e=e.substring(G.length+1),d=A.slice(f)}}i.loose||(a?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(a=!0)),i.items.push({type:"list_item",raw:p,task:!!this.options.gfm&&this.rules.other.listIsTask.test(c),loose:!1,text:c,tokens:[]}),i.raw+=p}let o=i.items.at(-1);if(o)o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let l of i.items){if(this.lexer.state.top=!1,l.tokens=this.lexer.blockTokens(l.text,[]),l.task){if(l.text=l.text.replace(this.rules.other.listReplaceTask,""),l.tokens[0]?.type==="text"||l.tokens[0]?.type==="paragraph"){l.tokens[0].raw=l.tokens[0].raw.replace(this.rules.other.listReplaceTask,""),l.tokens[0].text=l.tokens[0].text.replace(this.rules.other.listReplaceTask,"");for(let c=this.lexer.inlineQueue.length-1;c>=0;c--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[c].src)){this.lexer.inlineQueue[c].src=this.lexer.inlineQueue[c].src.replace(this.rules.other.listReplaceTask,"");break}}let p=this.rules.other.listTaskCheckbox.exec(l.raw);if(p){let c={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};l.checked=c.checked,i.loose?l.tokens[0]&&["paragraph","text"].includes(l.tokens[0].type)&&"tokens"in l.tokens[0]&&l.tokens[0].tokens?(l.tokens[0].raw=c.raw+l.tokens[0].raw,l.tokens[0].text=c.raw+l.tokens[0].text,l.tokens[0].tokens.unshift(c)):l.tokens.unshift({type:"paragraph",raw:c.raw,text:c.raw,tokens:[c]}):l.tokens.unshift(c)}}if(!i.loose){let p=l.tokens.filter(d=>d.type==="space"),c=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=c}}if(i.loose)for(let l of i.items){l.loose=!0;for(let p of l.tokens)p.type==="text"&&(p.type="paragraph")}return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),r=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:r,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=J(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
37
- `):[],s={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===r.length){for(let a of r)this.rules.other.tableAlignRight.test(a)?s.align.push("right"):this.rules.other.tableAlignCenter.test(a)?s.align.push("center"):this.rules.other.tableAlignLeft.test(a)?s.align.push("left"):s.align.push(null);for(let a=0;a<n.length;a++)s.header.push({text:n[a],tokens:this.lexer.inline(n[a]),header:!0,align:s.align[a]});for(let a of i)s.rows.push(J(a,s.header.length).map((o,l)=>({text:o,tokens:this.lexer.inline(o),header:!1,align:s.align[l]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
38
- `?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=C(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else{let s=ge(t[2],"()");if(s===-2)return;if(s>-1){let o=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,o).trim(),t[3]=""}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3])}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),me(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return{type:"text",raw:s,text:s}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,a,o,l=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(a=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!a)continue;if(o=[...a].length,r[3]||r[4]){l+=o;continue}else if((r[5]||r[6])&&s%3&&!((s+o)%3)){p+=o;continue}if(l-=o,l>0)continue;o=Math.min(o,o+l+p);let d=[...r[0]][0].length,h=e.slice(0,s+r.index+d+o);if(Math.min(s,o)%2){let f=h.slice(1,-1);return{type:"em",raw:h,text:f,tokens:this.lexer.inlineTokens(f)}}let R=h.slice(2,-2);return{type:"strong",raw:h,text:R,tokens:this.lexer.inlineTokens(R)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e,t,n=""){let r=this.rules.inline.delLDelim.exec(e);if(!r)return;if(!(r[1]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,a,o,l=s,p=this.rules.inline.delRDelim;for(p.lastIndex=0,t=t.slice(-1*e.length+s);(r=p.exec(t))!=null;){if(a=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!a||(o=[...a].length,o!==s))continue;if(r[3]||r[4]){l+=o;continue}if(l-=o,l>0)continue;o=Math.min(o,o+l);let c=[...r[0]][0].length,d=e.slice(0,s+r.index+c+o),h=d.slice(s,-s);return{type:"del",raw:d,text:h,tokens:this.lexer.inlineTokens(h)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?r="http://"+t[0]:r=t[0]}return{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class u{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||T,this.options.tokenizer=this.options.tokenizer||new w,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:E.normal,inline:z.normal};this.options.pedantic?(t.block=E.pedantic,t.inline=z.pedantic):this.options.gfm&&(t.block=E.gfm,this.options.breaks?t.inline=z.breaks:t.inline=z.gfm),this.tokenizer.rules=t}static get rules(){return{block:E,inline:z}}static lex(e,t){return new u(t).lex(e)}static lexInline(e,t){return new u(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
39
- `),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let r;if(this.options.extensions?.block?.some(s=>(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=`
40
- `:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(`
41
- `)?"":`
42
- `)+r.raw,s.text+=`
43
- `+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(`
44
- `)?"":`
45
- `)+r.raw,s.text+=`
46
- `+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,a=e.slice(1),o;this.options.extensions.startBlock.forEach(l=>{o=l.call({lexer:this},a),typeof o=="number"&&o>=0&&(s=Math.min(s,o))}),s<1/0&&s>=0&&(i=e.substring(0,s+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=(s.raw.endsWith(`
47
- `)?"":`
48
- `)+r.raw,s.text+=`
49
- `+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=(s.raw.endsWith(`
50
- `)?"":`
51
- `)+r.raw,s.text+=`
52
- `+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let i;for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)i=r[2]?r[2].length:0,n=n.slice(0,r.index+i)+"["+"a".repeat(r[0].length-i-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let s=!1,a="";for(;e;){s||(a=""),s=!1;let o;if(this.options.extensions?.inline?.some(p=>(o=p.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let p=t.at(-1);o.type==="text"&&p?.type==="text"?(p.raw+=o.raw,p.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,a)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e,n,a)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let l=e;if(this.options.extensions?.startInline){let p=1/0,c=e.slice(1),d;this.options.extensions.startInline.forEach(h=>{d=h.call({lexer:this},c),typeof d=="number"&&d>=0&&(p=Math.min(p,d))}),p<1/0&&p>=0&&(l=e.substring(0,p+1))}if(o=this.tokenizer.inlineText(l)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(a=o.raw.slice(-1)),s=!0;let p=t.at(-1);p?.type==="text"?(p.raw+=o.raw,p.text+=o.text):t.push(o);continue}if(e){let p="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(p);break}else throw new Error(p)}}return t}};var y=class{options;parser;constructor(e){this.options=e||T}space(e){return""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
53
- `;return r?'<pre><code class="language-'+O(r)+'">'+(n?i:O(i,!0))+`</code></pre>
54
- `:"<pre><code>"+(n?i:O(i,!0))+`</code></pre>
55
- `}blockquote({tokens:e}){return`<blockquote>
56
- ${this.parser.parse(e)}</blockquote>
57
- `}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
58
- `}hr(e){return`<hr>
59
- `}list(e){let t=e.ordered,n=e.start,r="";for(let a=0;a<e.items.length;a++){let o=e.items[a];r+=this.listitem(o)}let i=t?"ol":"ul",s=t&&n!==1?' start="'+n+'"':"";return"<"+i+s+`>
60
- `+r+"</"+i+`>
61
- `}listitem(e){return`<li>${this.parser.parse(e.tokens)}</li>
62
- `}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox"> '}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
63
- `}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let r="";for(let i=0;i<e.rows.length;i++){let s=e.rows[i];n="";for(let a=0;a<s.length;a++)n+=this.tablecell(s[a]);r+=this.tablerow({text:n})}return r&&(r=`<tbody>${r}</tbody>`),`<table>
64
- <thead>
65
- `+t+`</thead>
66
- `+r+`</table>
67
- `}tablerow({text:e}){return`<tr>
68
- ${e}</tr>
69
- `}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
70
- `}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${O(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=X(e);if(i===null)return r;e=i;let s='<a href="'+e+'"';return t&&(s+=' title="'+O(t)+'"'),s+=">"+r+"</a>",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=X(e);if(i===null)return O(n);e=i;let s=`<img src="${e}" alt="${O(n)}"`;return t&&(s+=` title="${O(t)}"`),s+=">",s}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:O(e.text)}};var $=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}checkbox({raw:e}){return e}};var b=class u{options;renderer;textRenderer;constructor(e){this.options=e||T,this.options.renderer=this.options.renderer||new y,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new $}static parse(e,t){return new u(t).parse(e)}static parseInline(e,t){return new u(t).parseInline(e)}parse(e){let t="";for(let n=0;n<e.length;n++){let r=e[n];if(this.options.extensions?.renderers?.[r.type]){let s=r,a=this.options.extensions.renderers[s.type].call({parser:this},s);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","def","paragraph","text"].includes(s.type)){t+=a||"";continue}}let i=r;switch(i.type){case"space":{t+=this.renderer.space(i);break}case"hr":{t+=this.renderer.hr(i);break}case"heading":{t+=this.renderer.heading(i);break}case"code":{t+=this.renderer.code(i);break}case"table":{t+=this.renderer.table(i);break}case"blockquote":{t+=this.renderer.blockquote(i);break}case"list":{t+=this.renderer.list(i);break}case"checkbox":{t+=this.renderer.checkbox(i);break}case"html":{t+=this.renderer.html(i);break}case"def":{t+=this.renderer.def(i);break}case"paragraph":{t+=this.renderer.paragraph(i);break}case"text":{t+=this.renderer.text(i);break}default:{let s='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(s),"";throw new Error(s)}}}return t}parseInline(e,t=this.renderer){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let a=this.options.extensions.renderers[i.type].call({parser:this},i);if(a!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=a||"";continue}}let s=i;switch(s.type){case"escape":{n+=t.text(s);break}case"html":{n+=t.html(s);break}case"link":{n+=t.link(s);break}case"image":{n+=t.image(s);break}case"checkbox":{n+=t.checkbox(s);break}case"strong":{n+=t.strong(s);break}case"em":{n+=t.em(s);break}case"codespan":{n+=t.codespan(s);break}case"br":{n+=t.br(s);break}case"del":{n+=t.del(s);break}case"text":{n+=t.text(s);break}default:{let a='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(a),"";throw new Error(a)}}}return n}};var P=class{options;block;constructor(e){this.options=e||T}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens","emStrongMask"]);static passThroughHooksRespectAsync=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}emStrongMask(e){return e}provideLexer(){return this.block?x.lex:x.lexInline}provideParser(){return this.block?b.parse:b.parseInline}};var B=class{defaults=M();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=b;Renderer=y;TextRenderer=$;Lexer=x;Tokenizer=w;Hooks=P;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case"table":{let i=r;for(let s of i.header)n=n.concat(this.walkTokens(s.tokens,t));for(let s of i.rows)for(let a of s)n=n.concat(this.walkTokens(a.tokens,t));break}case"list":{let i=r;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=r;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(s=>{let a=i[s].flat(1/0);n=n.concat(this.walkTokens(a,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...a){let o=i.renderer.apply(this,a);return o===!1&&(o=s.apply(this,a)),o}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new y(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let a=s,o=n.renderer[a],l=i[a];i[a]=(...p)=>{let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c||""}}r.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new w(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let a=s,o=n.tokenizer[a],l=i[a];i[a]=(...p)=>{let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c}}r.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new P;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let a=s,o=n.hooks[a],l=i[a];P.passThroughHooks.has(s)?i[a]=p=>{if(this.defaults.async&&P.passThroughHooksRespectAsync.has(s))return(async()=>{let d=await o.call(i,p);return l.call(i,d)})();let c=o.call(i,p);return l.call(i,c)}:i[a]=(...p)=>{if(this.defaults.async)return(async()=>{let d=await o.apply(i,p);return d===!1&&(d=await l.apply(i,p)),d})();let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c}}r.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(a){let o=[];return o.push(s.call(this,a)),i&&(o=o.concat(i.call(this,a))),o}}this.defaults={...this.defaults,...r}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,r)=>{let i={...r},s={...this.defaults,...i},a=this.onError(!!s.silent,!!s.async);if(this.defaults.async===!0&&i.async===!1)return a(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return a(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return a(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(s.hooks&&(s.hooks.options=s,s.hooks.block=e),s.async)return(async()=>{let o=s.hooks?await s.hooks.preprocess(n):n,p=await(s.hooks?await s.hooks.provideLexer():e?x.lex:x.lexInline)(o,s),c=s.hooks?await s.hooks.processAllTokens(p):p;s.walkTokens&&await Promise.all(this.walkTokens(c,s.walkTokens));let h=await(s.hooks?await s.hooks.provideParser():e?b.parse:b.parseInline)(c,s);return s.hooks?await s.hooks.postprocess(h):h})().catch(a);try{s.hooks&&(n=s.hooks.preprocess(n));let l=(s.hooks?s.hooks.provideLexer():e?x.lex:x.lexInline)(n,s);s.hooks&&(l=s.hooks.processAllTokens(l)),s.walkTokens&&this.walkTokens(l,s.walkTokens);let c=(s.hooks?s.hooks.provideParser():e?b.parse:b.parseInline)(l,s);return s.hooks&&(c=s.hooks.postprocess(c)),c}catch(o){return a(o)}}}onError(e,t){return n=>{if(n.message+=`
71
- Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error occurred:</p><pre>"+O(n.message+"",!0)+"</pre>";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var L=new B;function g(u,e){return L.parse(u,e)}g.options=g.setOptions=function(u){return L.setOptions(u),g.defaults=L.defaults,H(g.defaults),g};g.getDefaults=M;g.defaults=T;g.use=function(...u){return L.use(...u),g.defaults=L.defaults,H(g.defaults),g};g.walkTokens=function(u,e){return L.walkTokens(u,e)};g.parseInline=L.parseInline;g.Parser=b;g.parser=b.parse;g.Renderer=y;g.TextRenderer=$;g.Lexer=x;g.lexer=x.lex;g.Tokenizer=w;g.Hooks=P;g.parse=g;var Ut=g.options,Kt=g.setOptions,Wt=g.use,Xt=g.walkTokens,Jt=g.parseInline,Vt=g,Yt=b.parse,en=x.lex;export{P as Hooks,x as Lexer,B as Marked,b as Parser,y as Renderer,$ as TextRenderer,w as Tokenizer,T as defaults,M as getDefaults,en as lexer,g as marked,Ut as options,Vt as parse,Jt as parseInline,Yt as parser,Kt as setOptions,Wt as use,Xt as walkTokens};
72
- //# sourceMappingURL=marked.esm.js.map