Commit Graph

32 Commits

Author SHA1 Message Date
086e485215 feat(editor): preservation schema, block reuse, toolbar, uploads and embeds
- preservationExtensions: raw HTML cards for unmodelled blocks (shown as
  text), figure with figcaption, the YouTube embed placeholder node, span
  marks, a global attribute map (never on* handlers), tables without
  colgroup or min-width, inline images without base64
- serializeDocument keeps the original bytes of every top-level block still
  equal to the loaded one, so editing one paragraph changes only that
  paragraph; tables serialize with thead and bare cell text
- Links accept only http, https, mailto and relative URLs (isPolicyUrl, the
  rule both sanitizer policies apply) and gain no target, rel or class
- Toolbar with headings, marks, lists, links, blockquote, table editing,
  images, embeds, raw HTML block, undo and redo; en and pl labels
- Images from the picker or a drop go out as summer-upload and are inserted
  on summer-upload-result with alt text and an optional caption
- editor-corpus.json: the editor own output, rebuilt and checked by
  corpus.test.ts (UPDATE_CORPUS=1 rewrites it) for the policy tests
- README Blog Editor section
2026-10-10 11:53:07 +02:00
47f3032dcf feat(editor): blog editor element with exact untouched value
- golem15-journal-blogeditor (BlogEditorElement): light-DOM element with a
  TipTap WYSIWYG tab and a Source tab that edits the exact HTML string
- The value is the exact served string until a user edit; external values
  go in with emitUpdate false; summer-change fires on user edits only and
  an echo set is a no-op
- StarterKit without the trailing node, links without target, rel or class;
  injectCSS false with the ProseMirror base rules in blogeditor.css under
  the element tag, colours from --c-* only
- Committed bundle embedded through admin.go; vitest smoke on an imported
  post fixture (footer, style block, spans, figure, table, entities)
2026-10-10 11:35:51 +02:00
234e6169cc build(editor): blog editor JS build and drift gate
- Private ESM package with exact pins: @tiptap/core, pm, starter-kit,
  extension-table and extension-image 3.31.4; vite 7.3.5, vitest 3.2.7,
  happy-dom 20.11.6 and typescript 5.9.3, the admin SPA toolchain
- vite.config.ts builds assets/src/blogeditor/index.ts into fixed-name
  assets/dist/blogeditor.js and blogeditor.css, minified, and runs vitest
  on happy-dom
- scripts/check-dist.sh rebuilds into a temp dir and fails when assets/dist
  is stale; node_modules is ignored
2026-10-10 11:35:44 +02:00
c60abc6d62 chore(deps): require summercms v0.1.17 2026-10-10 00:42:30 +02:00
5f47083450 fix(classes): treat duplicate attributes as a change
golang.org/x/net/html's tokenizer keeps only the first of repeated
attribute names, so FormatHTML's byte-preserving comparison saw
<a href="https://x" href="javascript:...">, <img src=ok
src="javascript:..."> and a repeated style as unchanged and stored the
raw bytes, and CheckHTML accepted them.

htmlTokens now scans each start tag's raw bytes for its attribute names,
following the tokenizer's own readTag rules, and reports the document as
changed when that list differs from the attributes the tokenizer kept.
Such input is stored as bluemonday's output, which keeps the first value
as browsers do, and CheckHTML refuses it. Input without repeated
attributes is still stored byte for byte. A differential run of 2M
random tags found no disagreement between the scan and the tokenizer.
2026-10-10 00:13:17 +02:00
0d00ec8e0d fix(api): sanitize translated content_html on every write
A translated content_html sent through the Journal API (syncTranslations
formatted only when content_html was empty) or through
journal:import-posts (no check) was stored verbatim and served by the
anonymous GET /_journal/api/v1/posts and /posts/{slug}. A translation
keyed on the default locale also wrote the host content_html column
past FormatHTML.

classes.FormatTranslation now sets content_html for every translated
locale before it is stored: rendered from a non-blank content with
FormatHTML (host-column parity; a supplied content_html is replaced),
otherwise a supplied content_html is sanitized with the new
classes.SanitizeHTML, which FormatHTML reuses. Both write paths call it.

PHP parity note: Winter's PostApiController::syncTranslations called
Post::formatHtml only when content_html was empty and otherwise stored
the supplied content_html verbatim; ImportPosts::syncTranslations stored
it verbatim; the MLJournalMarkdown admin widget always derived it from
content via formatHtml. The Go port now follows the admin widget on
every path. Response shapes and status codes for valid input are
unchanged; only the stored content_html of unsafe or content-mismatched
translations differs.
2026-10-10 00:09:47 +02:00
a3551f2bdf fix(classes): sanitize content_html with bluemonday instead of regex checks
- FormatHTML sanitizes the goldmark output with a bluemonday UGC policy
  extended only with what goldmark and the imported posts and categories
  use; unsafe markup is stripped, as Winter's Html::clean strips it
- HTML the policy keeps whole is stored byte for byte (token comparison)
- CheckHTML keeps its signature and 422 contract, refusing HTML the
  policy would change
- the three regexes are gone: the CR-01 bypasses are neutralized and
  "Data:" or "metadata:" text is no longer refused (WR-01)
2026-10-09 23:35:23 +02:00
03907ee387 test: FormatHTML keep and refuse coverage, admin re-save keeps HTML
- TestFormatHTMLKeepsRawHTML: imported-style bodies (paragraphs, attributes, https links and images, entities) round-trip; markdown keeps a trailing raw footer and style
- TestFormatHTMLRejectsUnsafeHTML: every unsafe case must return an error now that the gate is the only defence; adds svg onload, mixed-case schemes and tags, object, embed, script in an HTML block and raw data URLs
- TestPostsAdminResaveKeepsHTML: an admin update of HTML content keeps content_html
2026-10-09 22:48:59 +02:00
a675f2f197 chore(deps): require summercms v0.1.16 2026-10-09 21:32:49 +02:00
528d2d8451 fix(classes): keep raw HTML in content_html behind the safety gate
- FormatHTML renders with goldmark html.WithUnsafe(), as Winter's
  Markdown::parse passes raw HTML through, so an admin save, an import or
  an API write no longer turns imported HTML into raw HTML omitted
- rejectUnsafe still runs after the render and refuses script, iframe,
  object and embed tags, event handlers and javascript, vbscript and data
  URLs; API response shapes are unchanged
- TestFormatHTMLKeepsRawHTML covers HTML paragraphs and a raw footer
2026-10-09 20:58:49 +02:00
3d533d49f0 test(updates): expect category content in the Category.Fillable list pinned by TestJournalTables
TestJournalTables pins the exact Category.Fillable allow-list, like
models/fillable_test.go. The admin form needs content in Fillable (D-21).
v0.1.1
2026-10-09 16:26:58 +02:00
f22d08902e feat(admin): edit category content in the admin form behind the HTML gate
- add a content textarea (size huge) after description, with en and pl phrases
- add content to Category.Fillable
- export classes.CheckHTML and run it from FormBeforeCreate and FormBeforeUpdate:
  unsafe category content answers 422 on content, blank content is stored as NULL
2026-10-09 16:26:12 +02:00
740dcd086d feat(api): serve the category content field on GET /_journal/api/v1/categories
- add the nullable TEXT column golem15_journal_categories.content (migration 202610090001)
- map it as models.Category.Content and emit category content on every categories row
- document the categories row shape and the stored-as-is HTML semantics
2026-10-09 16:24:20 +02:00
57a347c97b feat(api): list an author's published posts at /authors/{author}/posts
- AuthorPosts reuses the /posts published predicate, per_page clamp, sort allow-list and row serializer
- Bearer tokens are ignored; drafts and scheduled posts never appear
- Both author routes share one resolve-or-respond helper for identical 404/500 paths
- Document query keys, envelope and 404 in README
2026-10-09 13:20:39 +02:00
908ade9c78 feat(api): anonymous author profile at /_journal/api/v1/authors/{author}
- Resolve {author} by golem15_bloghub_author_slug, then by login for users with a published post
- Project only id, names, login and author slug from backend_users; payload is id, slug, first_name, last_name
- JSON 404 {"error":"Author not found"} for unknown, soft-deleted and malformed values
- Document the route in README
2026-10-09 13:19:35 +02:00
Jakub Zych
f5cd5b16fe fix(admin): title the posts list Posts, not New post
The admin names a list page, its heading and breadcrumb after the first
side-menu item of its controller. The new_post item pointed at the posts
list too, came first and won. A side item cannot open the create form, so
drop it; the list toolbar's Create button stays.
2026-10-07 18:02:36 +02:00
Jakub Zych
9327e1e4f3 chore: pin v0.1.0 plugin dependencies v0.1.0 2026-10-07 11:11:07 +02:00
Jakub Zych
be4f33a72e fix(admin): apply Journal form follow-ups 2026-10-07 11:11:02 +02:00
Jakub Zych
d09edde080 test(15-04): complete PHPUnit map, migrations, FormatHTML, and search-gate tests
Named JOURNAL-001/002 fillable, FormatHTML unsafe tags, rollback/remigrate, RSS/categories/tags/media, and TestSearchGateOff with zero Typesense HTTP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 19:18:53 +02:00
Jakub Zych
c5b44b6a5d test(15-04): prove journal migrate-admin-list-write-draft path on Postgres
- Add TestJournalEndToEnd through party/surf/cabana on real Postgres
- Extract the admin harness and activate user+translate+journal in Requires order
- Anonymous list hides drafts; Bearer writes; JOURNAL-005 404 has no data key

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 19:11:52 +02:00
Jakub Zych
bdd1c1be61 feat(15-03): add journal media upload, RSS, and off-by-default Typesense gate
- POST /_journal/api/v1/media/upload requires access_posts and stays under journal/
- Anonymous GET rss is RSS 2.0 honoring rss_* settings; unpublished posts are omitted
- beachcomber Gate reads search_use_typesense (default false) so a fresh save makes no Typesense HTTP

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 19:03:35 +02:00
Jakub Zych
3a1dda45ec feat(15-03): enforce JOURNAL-005 draft 404 and backend-Bearer writes
- Show unpublished or future posts as 404 without a data key unless owner or access_other_posts
- POST/PUT/DELETE posts and featured-images require a backend-audience JWT and PHP {error} strings
- Anonymous GET categories and tags return the PHP list keys; frontend-audience tokens cannot write

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:59:12 +02:00
Jakub Zych
f2e5b3d072 feat(15-03): serve anonymous GET /_journal/api/v1/posts as a published list
- Public and write groups under /_journal/api/v1 with journal-public-api and journal-api buckets (Max 120)
- Index omits drafts and future published_at; per_page default 9 max 30
- Optional backend JWT on GET; missing Bearer on writes returns PHP {error:Authentication required}

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:54:41 +02:00
Jakub Zych
4ff98f7de8 feat(15-02): register journal export/import commands and Posts toolbar
JSON CLI matching PHP ExportPosts/ImportPosts ships as HasCommands, and Posts toolbar actions reuse that logic behind access_import_export.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:47:53 +02:00
Jakub Zych
ab06569641 fix(15-02): gofmt journal settings validation rules
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:42:39 +02:00
Jakub Zych
afd0a2cafc feat(15-02): add Categories, Tags, and journal settings admin
Register the remaining YAML admin screens and the settings singleton
so a host can boot Posts, Categories, Tags, and Code journal. Posts
update and delete refuse other authors without access_other_posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:42:29 +02:00
Jakub Zych
dea0934950 feat(15-02): add Posts admin with mlmarkdown and FormatHTML
YAML-driven Posts screen stores translatable markdown and regenerates
content_html in the plugin. Publish writes without access_publish are
refused; list and form queries stay on the principal without
access_other_posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:41:10 +02:00
Jakub Zych
0bf2d39323 fix(15-01): create backend_users before author_slug in models tests
TestPostTranslatableSmoke ran Plugin.Migrations() on an empty Postgres, so the author_slug ALTER failed. Match TestJournalTables and migrate lagoon backend_users first.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:32:20 +02:00
Jakub Zych
bb903a6850 feat(15-01): add journal en/pl phrasebooks and plugin README
Operators get UI catalogs for plugin, journal, post, category, and tag
keys, plus an application-neutral README for the compiled plugin.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:29:20 +02:00
Jakub Zych
9ae4b9141e feat(15-01): add journal categories, tags, settings, and author slug
Squash the remaining golem15_journal_* tables plus the backend_users
author slug so a host migrate matches the frozen PHP schema.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:28:17 +02:00
Jakub Zych
445022ef71 fix(15-01): keep models tests off the updates leaf
summer build rejects models importing a sibling updates package, and a
go.mod translate replace collides with the host workspace module path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:25:32 +02:00
Jakub Zych
54322128fb feat(15-01): clone journal plugin with translatable Post tracer
- Register golem15.journal requiring exactly golem15.translate
- Squash golem15_journal_posts DDL with unique slug and JSONB columns
- Persist one en/pl Post through translate helpers on real Postgres

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:22:47 +02:00