macron-cms 2.0.0 β†’ 3.0.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +376 -14
  3. data/app/assets/javascripts/macron/cms/codemirror.js +3 -0
  4. data/app/assets/javascripts/macron/cms/editor.js +34 -6
  5. data/app/assets/javascripts/macron/cms/katex.min.js +1 -0
  6. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  7. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  8. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  9. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  10. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  11. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Main-Bold.woff2 +0 -0
  12. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  13. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Main-Italic.woff2 +0 -0
  14. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Main-Regular.woff2 +0 -0
  15. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  16. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Math-Italic.woff2 +0 -0
  17. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  18. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  19. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  20. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Script-Regular.woff2 +0 -0
  21. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  22. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  23. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  24. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  25. data/app/assets/stylesheets/macron/cms/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  26. data/app/assets/stylesheets/macron/cms/katex.min.css +1 -0
  27. data/app/controllers/macron/cms/api/v1/articles_controller.rb +186 -0
  28. data/app/controllers/macron/cms/api/v1/base_controller.rb +35 -0
  29. data/app/controllers/macron/cms/api/v1/content_types_controller.rb +21 -0
  30. data/app/controllers/macron/cms/article_contents_controller.rb +14 -3
  31. data/app/helpers/macron/cms/fields_helper.rb +1 -1
  32. data/app/services/macron/cms/remote_asset_attacher.rb +86 -0
  33. data/app/views/layouts/macron/cms/application.html.erb +2 -0
  34. data/app/views/macron/cms/articles/_content_blocks_section.html.erb +5 -1
  35. data/app/views/macron/cms/fields/_file.html.erb +4 -0
  36. data/app/views/macron/cms/fields/_latex.html.erb +17 -0
  37. data/config/routes.rb +7 -0
  38. data/lib/macron/cms/configuration.rb +2 -1
  39. data/lib/macron/cms/version.rb +1 -1
  40. metadata +28 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8377dd550eb5e06468380e45e6b112ea5fccdf5a791d4c382a54cbf5c8273a22
4
- data.tar.gz: da9ca19a2ca4191b3591d59a594e436ae3db02686170f2186257e93c8d7d8e62
3
+ metadata.gz: 774dc3fc574e2fcd3d5e200b370bd6886718539fbab312cfe17632d1b18d153b
4
+ data.tar.gz: 003dbe77a32e518917f3f2c342319a27c701bad690d9a6acfbf009efaef91ce2
5
5
  SHA512:
6
- metadata.gz: '0379d659ad66a227790269275c41272f6a88076d06ecc88c59299b2db776f00fc1cbae6180a4cc1fa0d5bab115fae1dcb8c9223a044b4a3be9dadad262cb645f'
7
- data.tar.gz: 4d1dffb4a71491a89d10050f67ba69c313ce9fc0e3d1571071540ebc15f312aa75ba17b1ad840bff823b676c3621bc268143b4457e425e47ed0decb12c0912e1
6
+ metadata.gz: 75063868fbbbb013c613f307de1c6d790066eb146f9add543dbb3961df14f6ba98502a81a433e1adbd27bbd9304f774dac0dfc592564d92883b66bd7ea1e9b0a
7
+ data.tar.gz: 66ffde9ab34f2cf35255183e7a396f29db811100e858ee56aa6d85a1e565ab13dbe30f66d18e1ea61edfa82312b6bbb0b4d7ed25464674f2c1a0c3c882bbee05
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MacronCMS
2
2
 
3
- [![Gem Version](https://img.shields.io/badge/version-2.0.0-emerald.svg)](#version)
3
+ [![Gem Version](https://img.shields.io/badge/version-3.0.0-emerald.svg)](#version)
4
4
  [![Rails 8+](https://img.shields.io/badge/rails-8%2B-red.svg)](https://rubyonrails.org)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](MIT-LICENSE)
6
6
 
@@ -27,7 +27,7 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
27
27
  - **In-Place Block Inserters**: Add new blocks directly above, between, or below existing blocks on the Article management screen with one click.
28
28
  - **Inline Turbo Stream Updates**: Add, edit, and reorder content blocks with instantaneous Turbo Stream updatesβ€”no full-page reloads.
29
29
  - **Dynamic Field Generation**: Field controls are generated dynamically based on the content type schema and validated before saving into the block's `payload` JSONB column.
30
- - **7 Extensible Field Kinds**:
30
+ - **8 Extensible Field Kinds**:
31
31
  - `string`: Single-line text input.
32
32
  - `text`: Multiline text area.
33
33
  - `dropdown`: Select menu configured with custom option lists from the schema YAML.
@@ -35,6 +35,8 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
35
35
  - `markdown`: CodeMirror-backed markdown editor with formatted preview on the article show page.
36
36
  - `file`: Direct file attachment powered by Active Storage.
37
37
  - `json`: Dedicated JSON data editor.
38
+ - `latex`: CodeMirror LaTeX editor with `stex` syntax mode and KaTeX mathematical equation preview.
39
+ - **File Attachment Purging**: Remove or replace existing Active Storage attachments directly from the block editor with a dedicated purge checkbox, keeping payloads clean without recreating blocks.
38
40
 
39
41
  ### 🎨 Article Background & Typography Customization
40
42
  - **Custom Background Colors & Images**: Configure an optional background color or attach a background image directly to any article via Active Storage, with live image previews and one-click purge.
@@ -46,8 +48,9 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
46
48
  - **Article-Wide Text Color Propagation**: Set a custom text color at the article level that automatically propagates to the host view's `<body>` and header, as well as to child `WidgetComponent` instances (headings, paragraphs, and markdown prose).
47
49
  - **Public Style Helper**: Use `cms_article_background_style(@article)` to generate clean inline CSS applied directly to the host application's `<body>` tag.
48
50
 
49
- ### πŸ“ CodeMirror Markdown & Rich Media
50
- - **CodeMirror Markdown Editor**: Integrated CodeMirror editor in the CMS admin for writing Markdown content with syntax highlighting.
51
+ ### πŸ“ CodeMirror Markdown, LaTeX & Rich Media
52
+ - **CodeMirror Markdown & LaTeX Editors**: Integrated CodeMirror editors in the CMS admin for writing Markdown content and raw LaTeX expressions with syntax highlighting (`stex` mode).
53
+ - **Fast KaTeX Math Rendering**: Embedded KaTeX engine renders complex LaTeX equations, mathematical notation, and symbols instantaneously in both admin previews and front-end layouts.
51
54
  - **Formatted Markdown Previews**: Formatted GitHub Flavored Markdown (GFM) previews rendered directly on the article management page.
52
55
  - **Sandboxed Embeds**: Render third-party media and raw HTML snippets safely inside isolated `<iframe>` elements using `srcdoc` to prevent Active Storage download blocks and script collisions.
53
56
  - **Responsive Media Controls**: Configure image and embed widgets with responsive width options (`25%`, `33%`, `50%`, `66%`, `75%`, `100%`) and alignment (`left`, `center`, `right`).
@@ -70,6 +73,13 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
70
73
  - **Configurable Access Guard**: Protect the `/cms` engine namespace behind host application authentication.
71
74
  - **Role Customization**: Configure custom user models (`user_class`) and admin flags (`admin_flag`) via initializers.
72
75
 
76
+ ### πŸ€– REST API & AI Content Ingestion (`/cms/api/v1`)
77
+ - **Atomic Nested Import**: Create or update articles and complete sequences of typed content blocks in a single atomic transaction.
78
+ - **Automated Remote Asset Ingestion**: Remote image URLs (e.g. Substack, Medium, WordPress) are automatically downloaded, validated, and attached to Active Storage blobs.
79
+ - **Idempotent Upsert Support**: Re-run imports safely using `upsert: true` without creating duplicate articles.
80
+ - **Content Type Schema Introspection**: Query `GET /cms/api/v1/content_types` dynamically so AI agents know exact fields and widget types available.
81
+ - **Bearer Token Auth**: Secure endpoints with `Authorization: Bearer <token>` or `X-API-Key`.
82
+
73
83
  ### 🎨 Self-Contained Tailwind CSS v4
74
84
  - **Zero Host Setup Required**: MacronCMS packages its own standalone, precompiled Tailwind CSS v4 bundle (`app/assets/stylesheets/macron/cms/application.css`). Host applications do not need Tailwind installed to display the CMS admin interface.
75
85
  - **Isolated Styling**: The admin layout operates within its own styling boundary to avoid conflicts with host application design systems.
@@ -77,15 +87,11 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
77
87
  ### πŸ” Built-in SEO Metadata Helpers
78
88
  - **`cms_seo_meta(@article)`**: Helper to inject `<title>`, canonical `<link>`, and Open Graph meta tags into the view `<head>` using an article's title, meta title, and meta description overrides.
79
89
 
80
- ---
81
-
82
- ## Coming Soon
83
-
84
- ### 🌊 React & Three.js Hydration Plugins
85
- Drop in React plugins where heavy client-side interactivity is needed, making hydrating complex client componentsβ€”such as interactive 3D [Three.js](https://threejs.org/) scenes, interactive canvases, and custom calculatorsβ€”effortless and isolated.
86
-
87
- ### πŸ€– AI-Friendly API for Content Migration
88
- An AI-friendly ingest and transformation API for migrating existing content quickly and programmatically across legacy datasets, CMS exports, and unstructured assets.
90
+ ### 🌊 React & Three.js 3D Hydration
91
+ - **Interactive 3D Client Components**: Effortlessly hydrate rich client-side experiencesβ€”such as interactive 3D typography in [Three.js](https://threejs.org/)β€”wrapped cleanly in ViewComponent widgets.
92
+ - **Rich 3D Customization**: Supports selectable 3D fonts (Helvetiker, Optimer, Gentilis, Droid Serif), customizable font sizes, 3-axis rotation (`x`, `y`, `z`), and variable rotation speeds.
93
+ - **Visual CRT Shader Effects & Backgrounds**: Built-in CRT scanline shader overlays (intensity, density, speed) and dual-mode background support (hex colors or Active Storage background image in stretch/tile modes).
94
+ - **Turbo-Resilient Lifecycle**: Cleanly mounts, re-renders, and disposes React roots and Three.js animation loops across Turbo navigation (`turbo:load`, `turbo:render`, `turbo:before-cache`).
89
95
 
90
96
  ---
91
97
 
@@ -146,6 +152,8 @@ Scaffold content types using the built-in Rails generator:
146
152
  $ bin/rails generate macron:cms:content_type heading title:string level:dropdown color:color
147
153
  $ bin/rails generate macron:cms:content_type paragraph content:text
148
154
  $ bin/rails generate macron:cms:content_type markdown content:markdown
155
+ $ bin/rails generate macron:cms:content_type blockquote content:markdown
156
+ $ bin/rails generate macron:cms:content_type latex content:latex
149
157
  $ bin/rails generate macron:cms:content_type image file:file width:dropdown align:dropdown caption:string
150
158
  $ bin/rails generate macron:cms:content_type embed html:text width:dropdown align:dropdown
151
159
  ```
@@ -168,6 +176,7 @@ This generates:
168
176
  | `markdown` | Multiline markdown with CodeMirror editor | β€” | `field_name` |
169
177
  | `file` | File upload backed by Active Storage | β€” | `file` or `upload` (Active Storage attachment) |
170
178
  | `json` | Formatted JSON editor | β€” | `field_name` |
179
+ | `latex` | Mathematical formula editor with CodeMirror & KaTeX | β€” | `field_name` |
171
180
 
172
181
  ---
173
182
 
@@ -252,6 +261,91 @@ fields:
252
261
  - right
253
262
  ```
254
263
 
264
+ #### Mathematical Equation (`config/content_types/latex.yml`)
265
+ ```yaml
266
+ fields:
267
+ - name: content
268
+ kind: latex
269
+ ```
270
+
271
+ #### Blockquote with Markdown (`config/content_types/blockquote.yml`)
272
+ ```yaml
273
+ fields:
274
+ - name: content
275
+ kind: markdown
276
+ ```
277
+
278
+ #### Interactive 3D Quote (`config/content_types/three_js_quote.yml`)
279
+ ```yaml
280
+ fields:
281
+ - name: text
282
+ kind: string
283
+ - name: color
284
+ kind: color
285
+ - name: rotation_axis
286
+ kind: dropdown
287
+ options:
288
+ - name: X Axis
289
+ value: x
290
+ - name: Y Axis
291
+ value: y
292
+ - name: Z Axis
293
+ value: z
294
+ - name: rotation_speed
295
+ kind: dropdown
296
+ options:
297
+ - name: 0x (Paused)
298
+ value: "0"
299
+ - name: 0.5x
300
+ value: "0.5"
301
+ - name: 1x
302
+ value: "1"
303
+ - name: 2x
304
+ value: "2"
305
+ - name: font
306
+ kind: dropdown
307
+ options:
308
+ - name: Helvetiker (Sans-serif)
309
+ value: helvetiker
310
+ - name: Optimer (Modern Sans)
311
+ value: optimer
312
+ - name: Gentilis (Humanist Sans)
313
+ value: gentilis
314
+ - name: Droid Serif (Serif)
315
+ value: droid_serif
316
+ - name: canvas_height
317
+ kind: dropdown
318
+ options:
319
+ - name: Small (240px)
320
+ value: sm
321
+ - name: Medium (320px)
322
+ value: md
323
+ - name: Large (480px)
324
+ value: lg
325
+ - name: background_color
326
+ kind: color
327
+ - name: upload
328
+ kind: file
329
+ - name: background_mode
330
+ kind: dropdown
331
+ options:
332
+ - name: Stretch
333
+ value: stretch
334
+ - name: Tile
335
+ value: tile
336
+ - name: scanlines_intensity
337
+ kind: dropdown
338
+ options:
339
+ - name: Off
340
+ value: off
341
+ - name: Subtle
342
+ value: subtle
343
+ - name: Medium
344
+ value: medium
345
+ - name: Strong
346
+ value: strong
347
+ ```
348
+
255
349
  ---
256
350
 
257
351
  ### 4. Importing Content Types
@@ -298,9 +392,27 @@ Use `cms_seo_meta` for SEO metadata and `cms_article_background_style` to apply
298
392
  </article>
299
393
  ```
300
394
 
301
- In your application layout (`app/views/layouts/application.html.erb`), apply the `:body_style` slot:
395
+ In your application layout (`app/views/layouts/application.html.erb`), apply the `:body_style` slot and include the KaTeX and 3D widget assets:
302
396
 
303
397
  ```erb
398
+ <head>
399
+ <title><%= content_for(:title) || "Macron CMS" %></title>
400
+ <%= csrf_meta_tags %>
401
+ <%= csp_meta_tag %>
402
+ <%= turbo_include_tags %>
403
+
404
+ <%# KaTeX Stylesheet & Scripts (LaTeX Math Rendering) %>
405
+ <%= stylesheet_link_tag "macron/cms/katex.min", "data-turbo-track": "reload" %>
406
+ <%= javascript_include_tag "macron/cms/katex.min", "data-turbo-track": "reload", defer: true %>
407
+ <%= javascript_include_tag "widgets/latex", "data-turbo-track": "reload", defer: true %>
408
+
409
+ <%# React & Three.js Vendor Bundles (3D Interactive Widgets) %>
410
+ <%= javascript_include_tag "vendor/react_bundle", "data-turbo-track": "reload", defer: true %>
411
+ <%= javascript_include_tag "vendor/three_text_bundle", "data-turbo-track": "reload", defer: true %>
412
+ <%= javascript_include_tag "vendor/three_fonts_bundle", "data-turbo-track": "reload", defer: true %>
413
+ <%= javascript_include_tag "widgets/three_js_quote", "data-turbo-track": "reload", defer: true %>
414
+ </head>
415
+
304
416
  <body class="min-h-screen text-zinc-900 <%= 'bg-zinc-50' unless content_for?(:body_style) %>"
305
417
  style="<%= yield :body_style if content_for?(:body_style) %>">
306
418
  <%= yield %>
@@ -448,6 +560,255 @@ end
448
560
  </div>
449
561
  ```
450
562
 
563
+ #### Blockquote Widget Component Example
564
+
565
+ Render quote blocks with Markdown and dynamically inherited article text color:
566
+
567
+ ```ruby
568
+ # app/components/widgets/blockquote_component.rb
569
+ module Widgets
570
+ class BlockquoteComponent < Macron::Cms::WidgetComponent
571
+ def rendered_html
572
+ return "".html_safe if content.blank?
573
+
574
+ raw_html = Kramdown::Document.new(content.to_s, input: "GFM").to_html
575
+ helpers.sanitize(raw_html)
576
+ end
577
+
578
+ def custom_style
579
+ return unless article_text_color.present?
580
+
581
+ "color: #{article_text_color}; border-color: #{article_text_color};"
582
+ end
583
+
584
+ def blockquote_classes
585
+ base = "border-l-4 pl-4 py-1.5 my-6 max-w-none text-base leading-relaxed italic space-y-2"
586
+ article_text_color.present? ? base : "#{base} border-zinc-300 text-zinc-800"
587
+ end
588
+ end
589
+ end
590
+ ```
591
+
592
+ ```erb
593
+ <%# app/components/widgets/blockquote_component.html.erb %>
594
+ <blockquote class="<%= blockquote_classes %>" style="<%= custom_style %>">
595
+ <%= rendered_html %>
596
+ </blockquote>
597
+ ```
598
+
599
+ #### LaTeX Mathematical Widget Component Example
600
+
601
+ Render mathematical notation and formulas with KaTeX:
602
+
603
+ ```ruby
604
+ # app/components/widgets/latex_component.rb
605
+ module Widgets
606
+ class LatexComponent < Macron::Cms::WidgetComponent
607
+ def formula
608
+ content.to_s
609
+ end
610
+
611
+ def custom_style
612
+ "color: #{article_text_color};" if article_text_color.present?
613
+ end
614
+
615
+ def latex_classes
616
+ "cms-latex-widget my-6 overflow-x-auto py-2 text-center"
617
+ end
618
+ end
619
+ end
620
+ ```
621
+
622
+ ```erb
623
+ <%# app/components/widgets/latex_component.html.erb %>
624
+ <% if formula.present? %>
625
+ <%= content_tag(
626
+ :div,
627
+ content_tag(:span, formula, class: "font-mono text-xs opacity-75"),
628
+ class: latex_classes,
629
+ style: custom_style,
630
+ data: { cms_latex: formula, cms_latex_display: "true" }
631
+ ) %>
632
+ <% end %>
633
+ ```
634
+
635
+ #### Interactive 3D Three.js Widget Component Example
636
+
637
+ Hydrate a 3D Three.js scene with React in a ViewComponent with Turbo lifecycle awareness:
638
+
639
+ ```ruby
640
+ # app/components/widgets/three_js_quote_component.rb
641
+ module Widgets
642
+ class ThreeJsQuoteComponent < Macron::Cms::WidgetComponent
643
+ DEFAULT_COLOR = "#18181b"
644
+ CANVAS_HEIGHT_CLASSES = {
645
+ "sm" => "h-60",
646
+ "md" => "h-64 sm:h-80",
647
+ "lg" => "h-96 sm:h-[480px]",
648
+ "xl" => "h-[540px] sm:h-[640px]"
649
+ }.freeze
650
+
651
+ def render?
652
+ quote_text.present?
653
+ end
654
+
655
+ def quote_text
656
+ field_value("text").to_s.strip.presence
657
+ end
658
+
659
+ def effective_color
660
+ field_value("color").presence || article_text_color.presence || DEFAULT_COLOR
661
+ end
662
+
663
+ def rotation_axis
664
+ field_value("rotation_axis").presence || "x"
665
+ end
666
+
667
+ def rotation_speed
668
+ field_value("rotation_speed").presence || "1"
669
+ end
670
+
671
+ def canvas_height_class
672
+ CANVAS_HEIGHT_CLASSES[field_value("canvas_height")] || "h-64 sm:h-80"
673
+ end
674
+ end
675
+ end
676
+ ```
677
+
678
+ ```erb
679
+ <%# app/components/widgets/three_js_quote_component.html.erb %>
680
+ <figure
681
+ class="my-6 w-full overflow-hidden rounded-xl border border-zinc-200 bg-gradient-to-b from-white to-zinc-50 shadow-xs"
682
+ data-three-js-quote-card
683
+ >
684
+ <blockquote class="sr-only"><%= quote_text %></blockquote>
685
+ <div
686
+ class="relative w-full <%= canvas_height_class %>"
687
+ data-three-js-quote-mount
688
+ data-quote-text="<%= quote_text %>"
689
+ data-quote-color="<%= effective_color %>"
690
+ data-rotation-axis="<%= rotation_axis %>"
691
+ data-rotation-speed="<%= rotation_speed %>"
692
+ data-font="<%= field_value('font').presence || 'helvetiker' %>"
693
+ data-font-size="<%= field_value('font_size').presence || 'md' %>"
694
+ data-scanlines-intensity="<%= field_value('scanlines_intensity').presence || 'off' %>"
695
+ role="img"
696
+ aria-label="<%= quote_text %>"
697
+ ></div>
698
+ </figure>
699
+ ```
700
+
701
+ ---
702
+
703
+ ## REST API & AI Content Ingestion
704
+
705
+ MacronCMS provides a JSON REST API under `/cms/api/v1/` designed for programmatic ingestion, automated scripts, and autonomous AI agents migrating content from external platforms (such as Substack, WordPress, or Medium).
706
+
707
+ ### Authentication
708
+
709
+ API endpoints are secured using a Bearer token or API key header:
710
+
711
+ ```http
712
+ Authorization: Bearer <MACRON_CMS_API_KEY>
713
+ ```
714
+ Or:
715
+ ```http
716
+ X-API-Key: <MACRON_CMS_API_KEY>
717
+ ```
718
+
719
+ Configure your key in an initializer (`config.api_key`) or via the `MACRON_CMS_API_KEY` environment variable. In `development` and `test` environments, authentication is optional if no key is configured.
720
+
721
+ ---
722
+
723
+ ### Endpoints
724
+
725
+ | Method | Endpoint | Description |
726
+ |---|---|---|
727
+ | `POST` | `/cms/api/v1/articles` | Create or upsert an article with nested content blocks |
728
+ | `GET` | `/cms/api/v1/articles/:id` | Retrieve article metadata and ordered content blocks (JSON) |
729
+ | `PATCH` | `/cms/api/v1/articles/:id` | Update article attributes and synchronize content blocks |
730
+ | `GET` | `/cms/api/v1/content_types` | Introspect available content types and schema field requirements |
731
+
732
+ ---
733
+
734
+ ### 1. Ingesting Articles with Nested Content Blocks
735
+
736
+ You can import an entire articleβ€”including its metadata, SEO tags, colors, and an ordered list of typed content blocksβ€”in a single atomic transaction:
737
+
738
+ ```bash
739
+ curl -X POST http://localhost:3000/cms/api/v1/articles \
740
+ -H "Content-Type: application/json" \
741
+ -H "Authorization: Bearer test-secret-key" \
742
+ -d '{
743
+ "upsert": true,
744
+ "article": {
745
+ "title": "The Myth of Your Limitations",
746
+ "name": "the-myth-of-your-limitations",
747
+ "meta_title": "The Myth of Your Limitations | Mindset Guide",
748
+ "meta_description": "How a simple mindset shift can prove you wrong.",
749
+ "background_color": "#ffffff",
750
+ "text_color": "#18181b"
751
+ },
752
+ "content_blocks": [
753
+ {
754
+ "content_type": "heading",
755
+ "payload": {
756
+ "title": "The Rule of Congruency",
757
+ "level": "h3"
758
+ }
759
+ },
760
+ {
761
+ "content_type": "markdown",
762
+ "payload": {
763
+ "content": "There is a principle in geometry called the **congruency of triangles**..."
764
+ }
765
+ },
766
+ {
767
+ "content_type": "image",
768
+ "payload": { "size": "lg" },
769
+ "image_url": "https://substack-post-media.s3.amazonaws.com/public/images/7343afc4-325e-42a1-9145-dd916d0de54d_1024x1024.png"
770
+ }
771
+ ]
772
+ }'
773
+ ```
774
+
775
+ #### Automated Remote Asset Ingestion
776
+ When an `image_url` (or `upload_url`) is provided in a content block, MacronCMS automatically downloads the asset from the remote source (e.g. Substack CDN), verifies content size and type, and attaches it as an Active Storage blob on the content block. If the remote server is unreachable, it gracefully falls back to storing the direct URL in the block payload.
777
+
778
+ #### Idempotent Upserting
779
+ When `"upsert": true` is specified, the API searches for an existing article matching `name`, `slug`, or `title`. If found, it updates the article attributes and atomically replaces its content blocks with the incoming sequence, making bulk migrations safe to re-run.
780
+
781
+ ---
782
+
783
+ ### 2. Schema Introspection (`GET /cms/api/v1/content_types`)
784
+
785
+ AI agents can query `/cms/api/v1/content_types` before generating payloads to dynamically discover which widget types exist and what fields they require:
786
+
787
+ ```json
788
+ {
789
+ "content_types": [
790
+ {
791
+ "id": 1,
792
+ "name": "heading",
793
+ "fields": [
794
+ { "name": "title", "kind": "string" },
795
+ { "name": "level", "kind": "dropdown", "options": ["h1", "h2", "h3", "h4", "h5", "h6"] },
796
+ { "name": "color", "kind": "color" }
797
+ ]
798
+ },
799
+ {
800
+ "id": 2,
801
+ "name": "markdown",
802
+ "fields": [
803
+ { "name": "content", "kind": "markdown" }
804
+ ]
805
+ }
806
+ ]
807
+ }
808
+ ```
809
+
810
+ For complete mapping rules from Substack HTML tags and a ready-to-use prompt template for AI agents, see [`docs/ai_importer_guide.md`](docs/ai_importer_guide.md).
811
+
451
812
  ---
452
813
 
453
814
  ## Configuration
@@ -459,6 +820,7 @@ Customize CMS options in an initializer:
459
820
  Macron::Cms.configure do |config|
460
821
  config.user_class = "User" # Devise user model (default: "User")
461
822
  config.admin_flag = :admin # Method or boolean column for admin check (default: :admin)
823
+ config.api_key = ENV["MACRON_CMS_API_KEY"] # Bearer token for /cms/api/v1 (default: ENV["MACRON_CMS_API_KEY"])
462
824
  config.content_types_path = Rails.root.join("config", "content_types") # Custom schema directory
463
825
  end
464
826
  ```
@@ -4,3 +4,6 @@
4
4
  !function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../meta")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../meta"],t):t(CodeMirror)}(function(w){"use strict";w.defineMode("markdown",function(m,d){var g=w.getMode(m,"text/html"),u="null"==g.name;void 0===d.highlightFormatting&&(d.highlightFormatting=!1),void 0===d.maxBlockquoteDepth&&(d.maxBlockquoteDepth=0),void 0===d.taskLists&&(d.taskLists=!1),void 0===d.strikethrough&&(d.strikethrough=!1),void 0===d.emoji&&(d.emoji=!1),void 0===d.fencedCodeBlockHighlighting&&(d.fencedCodeBlockHighlighting=!0),void 0===d.fencedCodeBlockDefaultMode&&(d.fencedCodeBlockDefaultMode="text/plain"),void 0===d.xml&&(d.xml=!0),void 0===d.tokenTypeOverrides&&(d.tokenTypeOverrides={});var t,c={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"image",imageAltText:"image-alt-text",imageMarker:"image-marker",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough",emoji:"builtin"};for(t in c)c.hasOwnProperty(t)&&d.tokenTypeOverrides[t]&&(c[t]=d.tokenTypeOverrides[t]);var f=/^([*\-_])(?:\s*\1){2,}\s*$/,k=/^(?:[*\-+]|^[0-9]+([.)]))\s+/,F=/^\[(x| )\](?=\s)/i,D=d.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,p=/^ {0,3}(?:\={1,}|-{2,})\s*$/,i=/^[^#!\[\]*_\\<>` "'(~:]+/,E=/^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/,x=/^\s*\[[^\]]+?\]:.*$/,A=/[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/;function C(t,e,i){return(e.f=e.inline=i)(t,e)}function S(t,e,i){return(e.f=e.block=i)(t,e)}function n(t){var e;return t.linkTitle=!1,t.linkHref=!1,t.linkText=!1,t.em=!1,t.strong=!1,t.strikethrough=!1,t.quote=0,t.indentedCode=!1,t.f==v&&(u?u:"xml"==(e=w.innerMode(g,t.htmlState)).mode.name&&null===e.state.tagStart&&!e.state.context&&e.state.tokenize.isInText)&&(t.f=T,t.block=a,t.htmlState=null),t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.prevLine=t.thisLine,t.thisLine={stream:null},null}function a(t,e){var i=t.column()===e.indentation,n=!(n=e.prevLine.stream)||!/\S/.test(n.string),u=e.indentedCode,a=e.prevLine.hr,r=!1!==e.list,o=(e.listStack[e.listStack.length-1]||0)+3,l=(e.indentedCode=!1,e.indentation);if(null===e.indentationDiff&&(e.indentationDiff=e.indentation,r)){for(e.list=null;l<e.listStack[e.listStack.length-1];)e.listStack.pop(),e.listStack.length?e.indentation=e.listStack[e.listStack.length-1]:e.list=!1;!1!==e.list&&(e.indentationDiff=l-e.listStack[e.listStack.length-1])}var h,s=!(n||a||e.prevLine.header||r&&u||e.prevLine.fencedCodeEnd),a=(!1===e.list||a||n)&&e.indentation<=o&&t.match(f),g=null;return 4<=e.indentationDiff&&(u||e.prevLine.fencedCodeEnd||e.prevLine.header||n)?(t.skipToEnd(),e.indentedCode=!0,c.code):t.eatSpace()?null:i&&e.indentation<=o&&(g=t.match(D))&&g[1].length<=6?(e.quote=0,e.header=g[1].length,e.thisLine.header=!0,d.highlightFormatting&&(e.formatting="header"),e.f=e.inline,L(e)):e.indentation<=o&&t.eat(">")?(e.quote=i?1:e.quote+1,d.highlightFormatting&&(e.formatting="quote"),t.eatSpace(),L(e)):!a&&!e.setext&&i&&e.indentation<=o&&(g=t.match(k))?(u=g[1]?"ol":"ul",e.indentation=l+t.current().length,e.list=!0,e.quote=0,e.listStack.push(e.indentation),e.em=!1,e.strong=!1,e.code=!1,e.strikethrough=!1,d.taskLists&&t.match(F,!1)&&(e.taskList=!0),e.f=e.inline,d.highlightFormatting&&(e.formatting=["list","list-"+u]),L(e)):i&&e.indentation<=o&&(g=t.match(E,!0))?(e.quote=0,e.fencedEndRE=new RegExp(g[1]+"+ *$"),e.localMode=d.fencedCodeBlockHighlighting&&(n=g[2]||d.fencedCodeBlockDefaultMode,w.findModeByName&&(h=w.findModeByName(n))&&(n=h.mime||h.mimes[0]),"null"==(h=w.getMode(m,n)).name?null:h),e.localMode&&(e.localState=w.startState(e.localMode)),e.f=e.block=B,d.highlightFormatting&&(e.formatting="code-block"),e.code=-1,L(e)):e.setext||!(s&&r||e.quote||!1!==e.list||e.code||a||x.test(t.string))&&(g=t.lookAhead(1))&&(g=g.match(p))?(e.setext?(e.header=e.setext,e.setext=0,t.skipToEnd(),d.highlightFormatting&&(e.formatting="header")):(e.header="="==g[0].charAt(0)?1:2,e.setext=e.header),e.thisLine.header=!0,e.f=e.inline,L(e)):a?(t.skipToEnd(),e.hr=!0,e.thisLine.hr=!0,c.hr):"["===t.peek()?C(t,e,b):C(t,e,e.inline)}function v(t,e){var i,n=g.token(t,e.htmlState);return u||("xml"==(i=w.innerMode(g,e.htmlState)).mode.name&&null===i.state.tagStart&&!i.state.context&&i.state.tokenize.isInText||e.md_inside&&-1<t.current().indexOf(">"))&&(e.f=T,e.block=a,e.htmlState=null),n}function B(t,e){var i,n=e.listStack[e.listStack.length-1]||0,u=e.indentation<n;return e.fencedEndRE&&e.indentation<=n+3&&(u||t.match(e.fencedEndRE))?(d.highlightFormatting&&(e.formatting="code-block"),u||(i=L(e)),e.localMode=e.localState=null,e.block=a,e.f=T,e.fencedEndRE=null,e.code=0,e.thisLine.fencedCodeEnd=!0,u?S(t,e,e.block):i):e.localMode?e.localMode.token(t,e.localState):(t.skipToEnd(),c.code)}function L(t){var e,i=[];if(t.formatting){i.push(c.formatting),"string"==typeof t.formatting&&(t.formatting=[t.formatting]);for(var n=0;n<t.formatting.length;n++)i.push(c.formatting+"-"+t.formatting[n]),"header"===t.formatting[n]&&i.push(c.formatting+"-"+t.formatting[n]+"-"+t.header),"quote"===t.formatting[n]&&(!d.maxBlockquoteDepth||d.maxBlockquoteDepth>=t.quote?i.push(c.formatting+"-"+t.formatting[n]+"-"+t.quote):i.push("error"))}return t.taskOpen?i.push("meta"):t.taskClosed?i.push("property"):(t.linkHref?i.push(c.linkHref,"url"):(t.strong&&i.push(c.strong),t.em&&i.push(c.em),t.strikethrough&&i.push(c.strikethrough),t.emoji&&i.push(c.emoji),t.linkText&&i.push(c.linkText),t.code&&i.push(c.code),t.image&&i.push(c.image),t.imageAltText&&i.push(c.imageAltText,"link"),t.imageMarker&&i.push(c.imageMarker)),t.header&&i.push(c.header,c.header+"-"+t.header),t.quote&&(i.push(c.quote),!d.maxBlockquoteDepth||d.maxBlockquoteDepth>=t.quote?i.push(c.quote+"-"+t.quote):i.push(c.quote+"-"+d.maxBlockquoteDepth)),!1!==t.list&&((e=(t.listStack.length-1)%3)?1==e?i.push(c.list2):i.push(c.list3):i.push(c.list1)),t.trailingSpaceNewLine?i.push("trailing-space-new-line"):t.trailingSpace&&i.push("trailing-space-"+(t.trailingSpace%2?"a":"b"))),i.length?i.join(" "):null}function e(t,e){if(t.match(i,!0))return L(e)}function T(t,e){var i=e.text(t,e);if(void 0!==i)return i;if(e.list)return e.list=null,L(e);if(e.taskList)return" "===t.match(F,!0)[1]?e.taskOpen=!0:e.taskClosed=!0,d.highlightFormatting&&(e.formatting="task"),e.taskList=!1,L(e);if(e.taskOpen=!1,e.taskClosed=!1,e.header&&t.match(/^#+$/,!0))return d.highlightFormatting&&(e.formatting="header"),L(e);var n=t.next();if(e.linkTitle){e.linkTitle=!1;var i=((i="("===n?")":n)+"").replace(/([.?*+^\[\]\\(){}|-])/g,"\\$1");if(t.match(new RegExp("^\\s*(?:[^"+i+"\\\\]+|\\\\\\\\|\\\\.)"+i),!0))return c.linkHref}if("`"===n)return i=e.formatting,d.highlightFormatting&&(e.formatting="code"),t.eatWhile("`"),o=t.current().length,0!=e.code||e.quote&&1!=o?o==e.code?(r=L(e),e.code=0,r):(e.formatting=i,L(e)):(e.code=o,L(e));if(e.code)return L(e);if("\\"===n&&(t.next(),d.highlightFormatting))return a=L(e),i=c.formatting+"-escape",a?a+" "+i:i;if("!"===n&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return e.imageMarker=!0,e.image=!0,d.highlightFormatting&&(e.formatting="image"),L(e);if("["===n&&e.imageMarker&&t.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/,!1))return e.imageMarker=!1,e.imageAltText=!0,d.highlightFormatting&&(e.formatting="image"),L(e);if("]"===n&&e.imageAltText)return d.highlightFormatting&&(e.formatting="image"),a=L(e),e.imageAltText=!1,e.image=!1,e.inline=e.f=q,a;if("["===n&&!e.image)return e.linkText&&t.match(/^.*?\]/)||(e.linkText=!0,d.highlightFormatting&&(e.formatting="link")),L(e);if("]"===n&&e.linkText)return d.highlightFormatting&&(e.formatting="link"),a=L(e),e.linkText=!1,e.inline=e.f=t.match(/\(.*?\)| ?\[.*?\]/,!1)?q:T,a;if("<"===n&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1))return e.f=e.inline=M,d.highlightFormatting&&(e.formatting="link"),(a=L(e))?a+=" ":a="",a+c.linkInline;if("<"===n&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1))return e.f=e.inline=M,d.highlightFormatting&&(e.formatting="link"),(a=L(e))?a+=" ":a="",a+c.linkEmail;if(d.xml&&"<"===n&&t.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i,!1))return-1!=(o=t.string.indexOf(">",t.pos))&&(i=t.string.substring(t.start,o),/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(i)&&(e.md_inside=!0)),t.backUp(1),e.htmlState=w.startState(g),S(t,e,v);if(d.xml&&"<"===n&&t.match(/^\/\w*?>/))return e.md_inside=!1,"tag";if("*"===n||"_"===n){for(var u=1,a=1==t.pos?" ":t.string.charAt(t.pos-2);u<3&&t.eat(n);)u++;var r,o=t.peek()||" ",i=!/\s/.test(o)&&(!A.test(o)||/\s/.test(a)||A.test(a)),l=!/\s/.test(a)&&(!A.test(a)||/\s/.test(o)||A.test(o)),h=null,s=null;if(u%2&&(e.em||!i||"*"!==n&&l&&!A.test(a)?e.em!=n||!l||"*"!==n&&i&&!A.test(o)||(h=!1):h=!0),1<u&&(e.strong||!i||"*"!==n&&l&&!A.test(a)?e.strong!=n||!l||"*"!==n&&i&&!A.test(o)||(s=!1):s=!0),null!=s||null!=h)return d.highlightFormatting&&(e.formatting=null==h?"strong":null==s?"em":"strong em"),!0===h&&(e.em=n),!0===s&&(e.strong=n),r=L(e),!1===h&&(e.em=!1),!1===s&&(e.strong=!1),r}else if(" "===n&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return L(e);t.backUp(1)}if(d.strikethrough)if("~"===n&&t.eatWhile(n)){if(e.strikethrough)return d.highlightFormatting&&(e.formatting="strikethrough"),r=L(e),e.strikethrough=!1,r;if(t.match(/^[^\s]/,!1))return e.strikethrough=!0,d.highlightFormatting&&(e.formatting="strikethrough"),L(e)}else if(" "===n&&t.match("~~",!0)){if(" "===t.peek())return L(e);t.backUp(2)}return d.emoji&&":"===n&&t.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)?(e.emoji=!0,d.highlightFormatting&&(e.formatting="emoji"),a=L(e),e.emoji=!1,a):(" "===n&&(t.match(/^ +$/,!1)?e.trailingSpace++:e.trailingSpace&&(e.trailingSpaceNewLine=!0)),L(e))}function M(t,e){return">"===t.next()?(e.f=e.inline=T,d.highlightFormatting&&(e.formatting="link"),(e=L(e))?e+=" ":e="",e+c.linkInline):(t.match(/^[^>]+/,!0),c.linkInline)}function q(t,e){if(t.eatSpace())return null;var n,t=t.next();return"("===t||"["===t?(e.f=e.inline=(n="("===t?")":"]",function(t,e){var i;return t.next()===n?(e.f=e.inline=T,d.highlightFormatting&&(e.formatting="link-string"),i=L(e),e.linkHref=!1,i):(t.match(r[n]),e.linkHref=!0,L(e))}),d.highlightFormatting&&(e.formatting="link-string"),e.linkHref=!0,L(e)):"error"}var r={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/};function b(t,e){return t.match(/^([^\]\\]|\\.)*\]:/,!1)?(e.f=o,t.next(),d.highlightFormatting&&(e.formatting="link"),e.linkText=!0,L(e)):C(t,e,T)}function o(t,e){var i;return t.match("]:",!0)?(e.f=e.inline=l,d.highlightFormatting&&(e.formatting="link"),i=L(e),e.linkText=!1,i):(t.match(/^([^\]\\]|\\.)+/,!0),c.linkText)}function l(t,e){return t.eatSpace()?null:(t.match(/^[^\s]+/,!0),void 0===t.peek()?e.linkTitle=!0:t.match(/^(?:\s+(?:"(?:[^"\\]|\\.)+"|'(?:[^'\\]|\\.)+'|\((?:[^)\\]|\\.)+\)))?/,!0),e.f=e.inline=T,c.linkHref+" url")}var h={startState:function(){return{f:a,prevLine:{stream:null},thisLine:{stream:null},block:a,htmlState:null,indentation:0,inline:T,text:e,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,setext:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,emoji:!1,fencedEndRE:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&w.copyState(g,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?w.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkText:t.linkText,linkTitle:t.linkTitle,linkHref:t.linkHref,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,emoji:t.emoji,header:t.header,setext:t.setext,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedEndRE:t.fencedEndRE}},token:function(t,e){if(e.formatting=!1,t!=e.thisLine.stream){if(e.header=0,e.hr=!1,t.match(/^\s*$/,!0))return n(e),null;if(e.prevLine=e.thisLine,e.thisLine={stream:t},e.taskList=!1,e.trailingSpace=0,e.trailingSpaceNewLine=!1,!e.localState&&(e.f=e.block,e.f!=v)){var i=t.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(e.indentation=i,e.indentationDiff=null,0<i)return null}}return e.f(t,e)},innerMode:function(t){return t.block==v?{state:t.htmlState,mode:g}:t.localState?{state:t.localState,mode:t.localMode}:{state:t,mode:h}},indent:function(t,e,i){return t.block==v&&g.indent?g.indent(t.htmlState,e,i):t.localState&&t.localMode.indent?t.localMode.indent(t.localState,e,i):w.Pass},blankLine:n,getType:L,blockCommentStart:"\x3c!--",blockCommentEnd:"--\x3e",closeBrackets:"()[]{}''\"\"``",fold:"markdown"};return h},"xml"),w.defineMIME("text/markdown","markdown"),w.defineMIME("text/x-markdown","markdown")});
5
5
  !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(rt){"use strict";rt.defineMode("javascript",function(e,l){var t,r,O,P,f=e.indentUnit,N=l.statementIndent,U=l.jsonld,o=l.json||U,W=!1!==l.trackScope,u=l.typescript,B=l.wordCharacters||/[\w$\xa1-\uffff]/,F=(e=n("keyword a"),t=n("keyword b"),r=n("keyword c"),O=n("keyword d"),P=n("operator"),{if:n("if"),while:e,with:e,else:t,do:t,try:t,finally:t,return:O,break:O,continue:O,new:n("new"),delete:r,void:r,throw:r,debugger:n("debugger"),var:n("var"),const:n("var"),let:n("var"),function:n("function"),catch:n("catch"),for:n("for"),switch:n("switch"),case:n("case"),default:n("default"),in:P,typeof:P,instanceof:P,true:e={type:"atom",style:"atom"},false:e,null:e,undefined:e,NaN:e,Infinity:e,this:n("this"),class:n("class"),super:n("atom"),yield:r,export:n("export"),import:n("import"),extends:r,await:r});function n(e){return{type:e,style:"keyword"}}var H,D,G=/[+\-*&%=<>!?|~^@]/,J=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;function i(e,t,r){return H=e,D=r,t}function d(e,t){var a,r=e.next();if('"'==r||"'"==r)return t.tokenize=(a=r,function(e,t){var r,n=!1;if(U&&"@"==e.peek()&&e.match(J))return t.tokenize=d,i("jsonld-keyword","meta");for(;null!=(r=e.next())&&(r!=a||n);)n=!n&&"\\"==r;return n||(t.tokenize=d),i("string","string")}),t.tokenize(e,t);if("."==r&&e.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/))return i("number","number");if("."==r&&e.match(".."))return i("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(r))return i(r);if("="==r&&e.eat(">"))return i("=>","operator");if("0"==r&&e.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/))return i("number","number");if(/\d/.test(r))return e.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/),i("number","number");if("/"==r)return e.eat("*")?(t.tokenize=K)(e,t):e.eat("/")?(e.skipToEnd(),i("comment","comment")):tt(e,t,1)?(function(e){for(var t,r=!1,n=!1;null!=(t=e.next());){if(!r){if("/"==t&&!n)return;"["==t?n=!0:n&&"]"==t&&(n=!1)}r=!r&&"\\"==t}}(e),e.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/),i("regexp","string-2")):(e.eat("="),i("operator","operator",e.current()));if("`"==r)return(t.tokenize=L)(e,t);if("#"==r&&"!"==e.peek())return e.skipToEnd(),i("meta","meta");if("#"==r&&e.eatWhile(B))return i("variable","property");if("<"==r&&e.match("!--")||"-"==r&&e.match("->")&&!/\S/.test(e.string.slice(0,e.start)))return e.skipToEnd(),i("comment","comment");if(G.test(r))return">"==r&&t.lexical&&">"==t.lexical.type||(e.eat("=")?"!"!=r&&"="!=r||e.eat("="):/[<>*+\-|&?]/.test(r)&&(e.eat(r),">"==r&&e.eat(r))),"?"==r&&e.eat(".")?i("."):i("operator","operator",e.current());if(B.test(r)){e.eatWhile(B);r=e.current();if("."!=t.lastType){if(F.propertyIsEnumerable(r))return i((t=F[r]).type,t.style,r);if("async"==r&&e.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/,!1))return i("async","keyword",r)}return i("variable","variable",r)}}function K(e,t){for(var r,n=!1;r=e.next();){if("/"==r&&n){t.tokenize=d;break}n="*"==r}return i("comment","comment")}function L(e,t){for(var r,n=!1;null!=(r=e.next());){if(!n&&("`"==r||"$"==r&&e.eat("{"))){t.tokenize=d;break}n=!n&&"\\"==r}return i("quasi","string-2",e.current())}function Q(e,t){t.fatArrowAt&&(t.fatArrowAt=null);var r=e.string.indexOf("=>",e.start);if(!(r<0)){!u||(n=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(e.string.slice(e.start,r)))&&(r=n.index);for(var n,a=0,i=!1,o=r-1;0<=o;--o){var c=e.string.charAt(o),s="([{}])".indexOf(c);if(0<=s&&s<3){if(!a){++o;break}if(0==--a){"("==c&&(i=!0);break}}else if(3<=s&&s<6)++a;else if(B.test(c))i=!0;else if(/["'\/`]/.test(c))for(;;--o){if(0==o)return;if(e.string.charAt(o-1)==c&&"\\"!=e.string.charAt(o-2)){o--;break}}else if(i&&!a){++o;break}}i&&!a&&(t.fatArrowAt=o)}}var R={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,import:!0,"jsonld-keyword":!0};function X(e,t,r,n,a,i){this.indented=e,this.column=t,this.type=r,this.prev=a,this.info=i,null!=n&&(this.align=n)}function Y(e,t,r,n,a){var i=e.cc;for(c.state=e,c.stream=a,c.marked=null,c.cc=i,c.style=t,e.lexical.hasOwnProperty("align")||(e.lexical.align=!0);;)if((i.length?i.pop():o?x:b)(r,n)){for(;i.length&&i[i.length-1].lex;)i.pop()();return c.marked?c.marked:"variable"==r&&function(e,t){if(W){for(var r=e.localVars;r;r=r.next)if(r.name==t)return 1;for(var n=e.context;n;n=n.prev)for(r=n.vars;r;r=r.next)if(r.name==t)return 1}}(e,n)?"variable-2":t}}var c={state:null,column:null,marked:null,cc:null};function s(){for(var e=arguments.length-1;0<=e;e--)c.cc.push(arguments[e])}function p(){return s.apply(null,arguments),!0}function Z(e,t){for(var r=t;r;r=r.next)if(r.name==e)return 1}function a(e){var t=c.state;if(c.marked="def",W){if(t.context)if("var"==t.lexical.info&&t.context&&t.context.block){var r=function e(t,r){{var n;return r?r.block?(n=e(t,r.prev))?n==r.prev?r:new te(n,r.vars,!0):null:Z(t,r.vars)?r:new te(r.prev,new re(t,r.vars),!1):null}}(e,t.context);if(null!=r)return void(t.context=r)}else if(!Z(e,t.localVars))return void(t.localVars=new re(e,t.localVars));l.globalVars&&!Z(e,t.globalVars)&&(t.globalVars=new re(e,t.globalVars))}}function ee(e){return"public"==e||"private"==e||"protected"==e||"abstract"==e||"readonly"==e}function te(e,t,r){this.prev=e,this.vars=t,this.block=r}function re(e,t){this.name=e,this.next=t}var ne=new re("this",new re("arguments",null));function m(){c.state.context=new te(c.state.context,c.state.localVars,!1),c.state.localVars=ne}function ae(){c.state.context=new te(c.state.context,c.state.localVars,!0),c.state.localVars=null}function k(){c.state.localVars=c.state.context.vars,c.state.context=c.state.context.prev}function v(n,a){function e(){var e=c.state,t=e.indented;if("stat"==e.lexical.type)t=e.lexical.indented;else for(var r=e.lexical;r&&")"==r.type&&r.align;r=r.prev)t=r.indented;e.lexical=new X(t,c.stream.column(),n,null,e.lexical,a)}return e.lex=!0,e}function y(){var e=c.state;e.lexical.prev&&(")"==e.lexical.type&&(e.indented=e.lexical.indented),e.lexical=e.lexical.prev)}function w(r){return function e(t){return t==r?p():";"==r||"}"==t||")"==t||"]"==t?s():p(e)}}function b(e,t){return"var"==e?p(v("vardef",t),qe,w(";"),y):"keyword a"==e?p(v("form"),oe,b,y):"keyword b"==e?p(v("form"),b,y):"keyword d"==e?c.stream.match(/^\s*$/,!1)?p():p(v("stat"),g,w(";"),y):"debugger"==e?p(w(";")):"{"==e?p(v("}"),ae,be,y,k):";"==e?p():"if"==e?("else"==c.state.lexical.info&&c.state.cc[c.state.cc.length-1]==y&&c.state.cc.pop()(),p(v("form"),oe,b,y,Oe)):"function"==e?p(q):"for"==e?p(v("form"),ae,Pe,b,k,y):"class"==e||u&&"interface"==t?(c.marked="keyword",p(v("form","class"==e?e:t),Fe,y)):"variable"==e?u&&"declare"==t?(c.marked="keyword",p(b)):u&&("module"==t||"enum"==t||"type"==t)&&c.stream.match(/^\s*\w/,!1)?(c.marked="keyword","enum"==t?p(Ze):"type"==t?p(We,w("operator"),z,w(";")):p(v("form"),T,w("{"),v("}"),be,y,y)):u&&"namespace"==t?(c.marked="keyword",p(v("form"),x,b,y)):u&&"abstract"==t?(c.marked="keyword",p(b)):p(v("stat"),me):"switch"==e?p(v("form"),oe,w("{"),v("}","switch"),ae,be,y,y,k):"case"==e?p(x,w(":")):"default"==e?p(w(":")):"catch"==e?p(v("form"),m,ie,b,y,k):"export"==e?p(v("stat"),Ge,y):"import"==e?p(v("stat"),Ke,y):"async"==e?p(b):"@"==t?p(x,b):s(v("stat"),x,w(";"),y)}function ie(e){if("("==e)return p(S,w(")"))}function x(e,t){return ce(e,t,!1)}function h(e,t){return ce(e,t,!0)}function oe(e){return"("!=e?s():p(v(")"),g,w(")"),y)}function ce(e,t,r){if(c.state.fatArrowAt==c.stream.start){var n=r?fe:le;if("("==e)return p(m,v(")"),V(S,")"),y,w("=>"),n,k);if("variable"==e)return s(m,T,w("=>"),n,k)}var a,n=r?M:j;return R.hasOwnProperty(e)?p(n):"function"==e?p(q,n):"class"==e||u&&"interface"==t?(c.marked="keyword",p(v("form"),Be,y)):"keyword c"==e||"async"==e?p(r?h:x):"("==e?p(v(")"),g,w(")"),y,n):"operator"==e||"spread"==e?p(r?h:x):"["==e?p(v("]"),Ye,y,n):"{"==e?we(ve,"}",null,n):"quasi"==e?s(se,n):"new"==e?p((a=r,function(e){return"."==e?p(a?pe:de):"variable"==e&&u?p(Ie,a?M:j):s(a?h:x)})):p()}function g(e){return e.match(/[;\}\)\],]/)?s():s(x)}function j(e,t){return","==e?p(g):M(e,t,!1)}function M(e,t,r){var n=0==r?j:M,a=0==r?x:h;return"=>"==e?p(m,r?fe:le,k):"operator"==e?/\+\+|--/.test(t)||u&&"!"==t?p(n):u&&"<"==t&&c.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/,!1)?p(v(">"),V(z,">"),y,n):"?"==t?p(x,w(":"),a):p(a):"quasi"==e?s(se,n):";"!=e?"("==e?we(h,")","call",n):"."==e?p(ke,n):"["==e?p(v("]"),g,w("]"),y,n):u&&"as"==t?(c.marked="keyword",p(z,n)):"regexp"==e?(c.state.lastType=c.marked="operator",c.stream.backUp(c.stream.pos-c.stream.start-1),p(a)):void 0:void 0}function se(e,t){return"quasi"!=e?s():"${"!=t.slice(t.length-2)?p(se):p(g,ue)}function ue(e){if("}"==e)return c.marked="string-2",c.state.tokenize=L,p(se)}function le(e){return Q(c.stream,c.state),s("{"==e?b:x)}function fe(e){return Q(c.stream,c.state),s("{"==e?b:h)}function de(e,t){if("target"==t)return c.marked="keyword",p(j)}function pe(e,t){if("target"==t)return c.marked="keyword",p(M)}function me(e){return":"==e?p(y,b):s(j,w(";"),y)}function ke(e){if("variable"==e)return c.marked="property",p()}function ve(e,t){return"async"==e?(c.marked="property",p(ve)):"variable"!=e&&"keyword"!=c.style?"number"==e||"string"==e?(c.marked=U?"property":c.style+" property",p(A)):"jsonld-keyword"==e?p(A):u&&ee(t)?(c.marked="keyword",p(ve)):"["==e?p(x,E,w("]"),A):"spread"==e?p(h,A):"*"==t?(c.marked="keyword",p(ve)):":"==e?s(A):void 0:(c.marked="property","get"==t||"set"==t?p(ye):(u&&c.state.fatArrowAt==c.stream.start&&(e=c.stream.match(/^\s*:\s*/,!1))&&(c.state.fatArrowAt=c.stream.pos+e[0].length),p(A)))}function ye(e){return"variable"!=e?s(A):(c.marked="property",p(q))}function A(e){return":"==e?p(h):"("==e?s(q):void 0}function V(n,a,i){function o(e,t){var r;return(i?-1<i.indexOf(e):","==e)?("call"==(r=c.state.lexical).info&&(r.pos=(r.pos||0)+1),p(function(e,t){return e==a||t==a?s():s(n)},o)):e==a||t==a?p():i&&-1<i.indexOf(";")?s(n):p(w(a))}return function(e,t){return e==a||t==a?p():s(n,o)}}function we(e,t,r){for(var n=3;n<arguments.length;n++)c.cc.push(arguments[n]);return p(v(t,r),V(e,t),y)}function be(e){return"}"==e?p():s(b,be)}function E(e,t){if(u)return":"==e?p(z):"?"==t?p(E):void 0}function xe(e,t){if(u&&(":"==e||"in"==t))return p(z)}function he(e){if(u&&":"==e)return c.stream.match(/^\s*\w+\s+is\b/,!1)?p(x,ge,z):p(z)}function ge(e,t){if("is"==t)return c.marked="keyword",p()}function z(e,t){return"keyof"==t||"typeof"==t||"infer"==t||"readonly"==t?(c.marked="keyword",p("typeof"==t?h:z)):"variable"==e||"void"==t?(c.marked="type",p(I)):"|"==t||"&"==t?p(z):"string"==e||"number"==e||"atom"==e?p(I):"["==e?p(v("]"),V(z,"]",","),y,I):"{"==e?p(v("}"),Me,y,I):"("==e?p(V(ze,")"),je,I):"<"==e?p(V(z,">"),z):"quasi"==e?s(Ve,I):void 0}function je(e){if("=>"==e)return p(z)}function Me(e){return e.match(/[\}\)\]]/)?p():","==e||";"==e?p(Me):s(Ae,Me)}function Ae(e,t){return"variable"==e||"keyword"==c.style?(c.marked="property",p(Ae)):"?"==t||"number"==e||"string"==e?p(Ae):":"==e?p(z):"["==e?p(w("variable"),xe,w("]"),Ae):"("==e?s(C,Ae):e.match(/[;\}\)\],]/)?void 0:p()}function Ve(e,t){return"quasi"!=e?s():"${"!=t.slice(t.length-2)?p(Ve):p(z,Ee)}function Ee(e){if("}"==e)return c.marked="string-2",c.state.tokenize=L,p(Ve)}function ze(e,t){return"variable"==e&&c.stream.match(/^\s*[?:]/,!1)||"?"==t?p(ze):":"==e?p(z):"spread"==e?p(ze):s(z)}function I(e,t){return"<"==t?p(v(">"),V(z,">"),y,I):"|"==t||"."==e||"&"==t?p(z):"["==e?p(z,w("]"),I):"extends"==t||"implements"==t?(c.marked="keyword",p(z)):"?"==t?p(z,w(":"),z):void 0}function Ie(e,t){if("<"==t)return p(v(">"),V(z,">"),y,I)}function Te(){return s(z,$e)}function $e(e,t){if("="==t)return p(z)}function qe(e,t){return"enum"==t?(c.marked="keyword",p(Ze)):s(T,E,$,_e)}function T(e,t){return u&&ee(t)?(c.marked="keyword",p(T)):"variable"==e?(a(t),p()):"spread"==e?p(T):"["==e?we(Se,"]"):"{"==e?we(Ce,"}"):void 0}function Ce(e,t){return"variable"!=e||c.stream.match(/^\s*:/,!1)?("variable"==e&&(c.marked="property"),"spread"==e?p(T):"}"==e?s():"["==e?p(x,w("]"),w(":"),Ce):p(w(":"),T,$)):(a(t),p($))}function Se(){return s(T,$)}function $(e,t){if("="==t)return p(h)}function _e(e){if(","==e)return p(qe)}function Oe(e,t){if("keyword b"==e&&"else"==t)return p(v("form","else"),b,y)}function Pe(e,t){return"await"==t?p(Pe):"("==e?p(v(")"),Ne,y):void 0}function Ne(e){return"var"==e?p(qe,Ue):("variable"==e?p:s)(Ue)}function Ue(e,t){return")"==e?p():";"==e?p(Ue):"in"==t||"of"==t?(c.marked="keyword",p(x,Ue)):s(x,Ue)}function q(e,t){return"*"==t?(c.marked="keyword",p(q)):"variable"==e?(a(t),p(q)):"("==e?p(m,v(")"),V(S,")"),y,he,b,k):u&&"<"==t?p(v(">"),V(Te,">"),y,q):void 0}function C(e,t){return"*"==t?(c.marked="keyword",p(C)):"variable"==e?(a(t),p(C)):"("==e?p(m,v(")"),V(S,")"),y,he,k):u&&"<"==t?p(v(">"),V(Te,">"),y,C):void 0}function We(e,t){return"keyword"==e||"variable"==e?(c.marked="type",p(We)):"<"==t?p(v(">"),V(Te,">"),y):void 0}function S(e,t){return"@"==t&&p(x,S),"spread"==e?p(S):u&&ee(t)?(c.marked="keyword",p(S)):u&&"this"==e?p(E,$):s(T,E,$)}function Be(e,t){return("variable"==e?Fe:He)(e,t)}function Fe(e,t){if("variable"==e)return a(t),p(He)}function He(e,t){return"<"==t?p(v(">"),V(Te,">"),y,He):"extends"==t||"implements"==t||u&&","==e?("implements"==t&&(c.marked="keyword"),p(u?z:x,He)):"{"==e?p(v("}"),_,y):void 0}function _(e,t){return"async"==e||"variable"==e&&("static"==t||"get"==t||"set"==t||u&&ee(t))&&c.stream.match(/^\s+#?[\w$\xa1-\uffff]/,!1)?(c.marked="keyword",p(_)):"variable"==e||"keyword"==c.style?(c.marked="property",p(De,_)):"number"==e||"string"==e?p(De,_):"["==e?p(x,E,w("]"),De,_):"*"==t?(c.marked="keyword",p(_)):u&&"("==e?s(C,_):";"==e||","==e?p(_):"}"==e?p():"@"==t?p(x,_):void 0}function De(e,t){if("!"==t)return p(De);if("?"==t)return p(De);if(":"==e)return p(z,$);if("="==t)return p(h);e=c.state.lexical.prev;return s(e&&"interface"==e.info?C:q)}function Ge(e,t){return"*"==t?(c.marked="keyword",p(Xe,w(";"))):"default"==t?(c.marked="keyword",p(x,w(";"))):"{"==e?p(V(Je,"}"),Xe,w(";")):s(b)}function Je(e,t){return"as"==t?(c.marked="keyword",p(w("variable"))):"variable"==e?s(h,Je):void 0}function Ke(e){return"string"==e?p():"("==e?s(x):"."==e?s(j):s(Le,Qe,Xe)}function Le(e,t){return"{"==e?we(Le,"}"):("variable"==e&&a(t),"*"==t&&(c.marked="keyword"),p(Re))}function Qe(e){if(","==e)return p(Le,Qe)}function Re(e,t){if("as"==t)return c.marked="keyword",p(Le)}function Xe(e,t){if("from"==t)return c.marked="keyword",p(x)}function Ye(e){return"]"==e?p():s(V(h,"]"))}function Ze(){return s(v("form"),T,w("{"),v("}"),V(et,"}"),y,y)}function et(){return s(T,$)}function tt(e,t,r){return t.tokenize==d&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(t.lastType)||"quasi"==t.lastType&&/\{\s*$/.test(e.string.slice(0,e.pos-(r||0)))}return m.lex=ae.lex=!0,y.lex=k.lex=!0,{startState:function(e){e={tokenize:d,lastType:"sof",cc:[],lexical:new X((e||0)-f,0,"block",!1),localVars:l.localVars,context:l.localVars&&new te(null,null,!1),indented:e||0};return l.globalVars&&"object"==typeof l.globalVars&&(e.globalVars=l.globalVars),e},token:function(e,t){if(e.sol()&&(t.lexical.hasOwnProperty("align")||(t.lexical.align=!1),t.indented=e.indentation(),Q(e,t)),t.tokenize!=K&&e.eatSpace())return null;var r=t.tokenize(e,t);return"comment"==H?r:(t.lastType="operator"!=H||"++"!=D&&"--"!=D?H:"incdec",Y(t,r,H,D,e))},indent:function(e,t){if(e.tokenize==K||e.tokenize==L)return rt.Pass;if(e.tokenize!=d)return 0;var r,n=t&&t.charAt(0),a=e.lexical;if(!/^\s*else\b/.test(t))for(var i=e.cc.length-1;0<=i;--i){var o=e.cc[i];if(o==y)a=a.prev;else if(o!=Oe&&o!=k)break}for(;("stat"==a.type||"form"==a.type)&&("}"==n||(r=e.cc[e.cc.length-1])&&(r==j||r==M)&&!/^[,\.=+\-*:?[\(]/.test(t));)a=a.prev;var c,s=(a=N&&")"==a.type&&"stat"==a.prev.type?a.prev:a).type,u=n==s;return"vardef"==s?a.indented+("operator"==e.lastType||","==e.lastType?a.info.length+1:0):"form"==s&&"{"==n?a.indented:"form"==s?a.indented+f:"stat"==s?a.indented+(s=t,"operator"==(c=e).lastType||","==c.lastType||G.test(s.charAt(0))||/[,.]/.test(s.charAt(0))?N||f:0):"switch"!=a.info||u||0==l.doubleIndentSwitch?a.align?a.column+(u?0:1):a.indented+(u?0:f):a.indented+(/^(?:case|default)\b/.test(t)?f:2*f)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:o?null:"/*",blockCommentEnd:o?null:"*/",blockCommentContinue:o?null:" * ",lineComment:o?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:o?"json":"javascript",jsonldMode:U,jsonMode:o,expressionAllowed:tt,skipExpression:function(e){Y(e,"atom","atom","true",new rt.StringStream("",2,null))}}}),rt.registerHelper("wordChars","javascript",/[\w$]/),rt.defineMIME("text/javascript","javascript"),rt.defineMIME("text/ecmascript","javascript"),rt.defineMIME("application/javascript","javascript"),rt.defineMIME("application/x-javascript","javascript"),rt.defineMIME("application/ecmascript","javascript"),rt.defineMIME("application/json",{name:"javascript",json:!0}),rt.defineMIME("application/x-json",{name:"javascript",json:!0}),rt.defineMIME("application/manifest+json",{name:"javascript",json:!0}),rt.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),rt.defineMIME("text/typescript",{name:"javascript",typescript:!0}),rt.defineMIME("application/typescript",{name:"javascript",typescript:!0})});
6
6
  !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(o){"use strict";o.overlayMode=function(a,n,t){return{startState:function(){return{base:o.startState(a),overlay:o.startState(n),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(e){return{base:o.copyState(a,e.base),overlay:o.copyState(n,e.overlay),basePos:e.basePos,baseCur:null,overlayPos:e.overlayPos,overlayCur:null}},token:function(e,o){return(e!=o.streamSeen||Math.min(o.basePos,o.overlayPos)<e.start)&&(o.streamSeen=e,o.basePos=o.overlayPos=e.start),e.start==o.basePos&&(o.baseCur=a.token(e,o.base),o.basePos=e.pos),e.start==o.overlayPos&&(e.pos=e.start,o.overlayCur=n.token(e,o.overlay),o.overlayPos=e.pos),e.pos=Math.min(o.basePos,o.overlayPos),null==o.overlayCur?o.baseCur:null!=o.baseCur&&o.overlay.combineTokens||t&&null==o.overlay.combineTokens?o.baseCur+" "+o.overlayCur:o.overlayCur},indent:a.indent&&function(e,o,r){return a.indent(e.base,o,r)},electricChars:a.electricChars,innerMode:function(e){return{state:e.base,mode:a}},blankLine:function(e){var o,r;return a.blankLine&&(o=a.blankLine(e.base)),null==(r=n.blankLine?n.blankLine(e.overlay):r)?o:t&&null!=o?o+" "+r:r}}}});
7
+
8
+ /* CodeMirror sTeX / LaTeX Mode */
9
+ !function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(CodeMirror){"use strict";CodeMirror.defineMode("stex",function(){return{startState:function(){return{cmdState:[]};},copyState:function(s){return{cmdState:s.cmdState.slice(0)};},token:function(source,state){if(source.eatSpace())return null;if(source.match(/^\\[a-zA-Z@]+/)){var cmd=source.current().slice(1);if(cmd==="begin"||cmd==="end")return"keyword";return"builtin";}if(source.match(/^\\[^a-zA-Z@]/))return"atom";if(source.match(/^\$\$|\$|\\\[|\\\]|\\\(|\\\)/))return"string-2";if(source.match(/^%[^\n]*/))return"comment";if(source.match(/^[0-9]+(\.[0-9]+)?/))return"number";if(source.match(/^[{}[\]()]/))return"bracket";if(source.match(/^[-+*\/=<>!^_,|~&:]/))return"operator";source.next();return null;},lineComment:"%"};});CodeMirror.defineMIME("text/x-stex","stex");CodeMirror.defineMIME("text/x-latex","stex");});
@@ -6,6 +6,8 @@
6
6
  return "markdown";
7
7
  case "json":
8
8
  return { name: "javascript", json: true };
9
+ case "latex":
10
+ return "stex";
9
11
  default:
10
12
  return "text/plain";
11
13
  }
@@ -78,22 +80,48 @@
78
80
  }
79
81
  }
80
82
 
83
+ function renderAllLatex(root) {
84
+ if (!window.katex) return;
85
+ var scope = root && root.querySelectorAll ? root : document;
86
+ var elements = scope.querySelectorAll("[data-cms-latex]");
87
+ for (var i = 0; i < elements.length; i++) {
88
+ var el = elements[i];
89
+ var formula = el.getAttribute("data-cms-latex") || el.textContent;
90
+ if (!formula) continue;
91
+ try {
92
+ var isDisplay = el.getAttribute("data-cms-latex-display") !== "false";
93
+ window.katex.render(formula, el, {
94
+ displayMode: isDisplay,
95
+ throwOnError: false
96
+ });
97
+ } catch (err) {
98
+ console.error("KaTeX render error:", err);
99
+ }
100
+ }
101
+ }
102
+
81
103
  window.MacronCms = window.MacronCms || {};
82
104
  window.MacronCms.initEditors = initCmsEditors;
83
105
  window.MacronCms.refreshEditors = refreshAllEditors;
106
+ window.MacronCms.renderLatex = renderAllLatex;
107
+
108
+ function initAll(root) {
109
+ initCmsEditors(root);
110
+ renderAllLatex(root);
111
+ }
84
112
 
85
113
  // Listen to Turbo & DOM lifecycle events
86
- document.addEventListener("DOMContentLoaded", function() { initCmsEditors(); });
87
- document.addEventListener("turbo:load", function() { initCmsEditors(); });
88
- document.addEventListener("turbo:render", function() { initCmsEditors(); });
89
- document.addEventListener("turbo:frame-load", function() { initCmsEditors(); });
114
+ document.addEventListener("DOMContentLoaded", function() { initAll(); });
115
+ document.addEventListener("turbo:load", function() { initAll(); });
116
+ document.addEventListener("turbo:render", function() { initAll(); });
117
+ document.addEventListener("turbo:frame-load", function() { initAll(); });
90
118
 
91
- // Listen for clicks that might toggle hidden forms and refresh editors
119
+ // Listen for clicks that might toggle hidden forms and refresh editors/render latex
92
120
  document.addEventListener("click", function(e) {
93
121
  var target = e.target;
94
122
  if (target && (target.closest("button") || target.closest("a"))) {
95
123
  setTimeout(function() {
96
- initCmsEditors();
124
+ initAll();
97
125
  refreshAllEditors();
98
126
  }, 50);
99
127
  }