API / Minn\Content

Minn\Content

The repositories and records: posts, users, terms, comments, and the render pipeline. 42 classes, 296 public methods and 80 internals. This page is the public surface; each class name opens the whole class.

Autop final class

public/minn/src/Minn/Content/Autop.php · 168 lines · Open the class →

Classic-content paragraphing (wpautop): blank lines become paragraphs, single newlines become line breaks when asked, and block-level tags are never wrapped. Rebuilt against the reference's output on a corpus of fuzzed and real content plus targeted experiments (probe autop); every rule below is one that corpus demanded, written as the behaviour seen, in the order it applies:

- <pre> elements are cut out first and put back untouched at the end. - A pair of <br> tags is a paragraph break; block tags get blank lines around them, so they always sit outside paragraphs. - Newlines inside a tag, comment or CDATA section (an unclosed one runs to the end) are held aside, so they never split a paragraph and come back as plain newlines. While held, such a newline ends the tag as far as the block rules can tell. - Whitespace around object, param, embed, audio, video, source, track, figcaption and option tags is squeezed out when those tags are used. - Blank-line separated runs become <p> paragraphs, then paragraphs that only hold a block tag lose their <p> and </p>. - With line breaks on, remaining newlines become <br />, except inside script, style, svg and math elements, and breaks next to block tags go.

staticapply(string $text, bool $lineBreaks = true): string

The classic paragraph rules: a blank line makes a paragraph, a single newline a line break when asked.

insidestashPre(), spaceBlocks(), holdTagNewlines(), squeezeMedia(), squeezeFormParts(), wrapParagraphs(), tidyParagraphs(), addLineBreaks(), tidyBreaks()

Blocks final class

public/minn/src/Minn/Content/Blocks.php · 46 lines · Open the class →

The content pipeline's front door: block markup goes through the block renderer (Minn\Blocks), classic content rides the paragraph pipeline.

staticrender(string $raw): string

Post content as HTML: blocks through the renderer, classic content through the paragraph rules.

staticrenderer(): Minn\Blocks\Renderer

The block renderer of the request being answered, which its runtime holds and builds over that request's own database door. With no runtime (the command line, the unit suite) one off-request renderer stands in.

staticparagraphs(string $raw): string

Plain prose to paragraphs: texturize, split on blank lines, <br /> on single newlines. Shared by classic post content and comment text.

Clickable final class

public/minn/src/Minn/Content/Clickable.php · 64 lines · Open the class →

Bare addresses in text made links (make_clickable, probe formatting-callbacks). Only text is read, never tags or comments, and nothing inside code, pre, script or style. An address must start the text or follow whitespace or "("; trailing . , ; : ! ? stay outside it. URLs (http, https, ftp) are found first, then www. and ftp. hosts, then email addresses; each is handed to its callback, which builds the link. A link made as the whole text of an existing link is unwrapped into it.

staticmake(string $text, Closure $url, Closure $web, Closure $email): string

The text with its addresses made links: $url gets [whole, before, address, trailing], $web the same for a www. or ftp. host, $email [whole, before, user, domain].

@param Closure(list<string>): string $url
@param Closure(list<string>): string $web
@param Closure(list<string>): string $email
staticunbalancedClosers(string $url): array

A URL's trailing ")" taken off while the URL closes more parentheses than it opens: [the URL, what was taken off].

@return array{0: string, 1: string}
insidelinked(), unwrapNested()

CommentClasses final class

public/minn/src/Minn/Content/CommentClasses.php · 26 lines · Open the class →

The class tokens a rendered comment carries: its type, its author, odd/even and thread alternation, depth, then the caller's extras.

staticbuild(string $type, ?string $authorClass, bool $byPostAuthor, int $alt, int $depth, int $threadAlt, array $extra): array

The class list a comment's list item carries, in the reference's order.

@param list<string> $extra
@return list<string>

CommentRecord final readonly class

public/minn/src/Minn/Content/CommentRecord.php · 114 lines · implements ArrayAccess · Open the class →

One row of the comments table, read by name: $comment->author, ->content, ->postId, ->parentId, and isApproved() for the status the reference stores as '1'. Array access is the migration bridge, read-only.

propreadonly int $id
propreadonly int $postId
propreadonly string $author
propreadonly string $authorEmail
propreadonly string $authorUrl
propreadonly string $authorIp
propreadonly string $date
propreadonly string $dateGmt
propreadonly string $content
propreadonly int $karma
propreadonly string $approved
propreadonly string $agent
propreadonly string $type
propreadonly int $parentId
propreadonly int $userId
staticfromRow(array $row): self

A record from a comments row; a missing column reads as empty.

@param array<string, mixed> $row
staticfromRows(array $rows): array

A record for every row, in order.

@param list<array<string, mixed>> $rows @return list<self>
methodrow(): array

The stored row, for the writers and shapers that still spell columns.

methodisApproved(): bool

Approved, and so public.

methodisPending(): bool

Held for moderation.

methodisComment(): bool

A plain comment: the type column is '' on old rows and 'comment' on new ones.

methodcolumn(string $name): mixed

One stored column by its database name, or null.

methodoffsetExists(mixed $offset): bool

The migration bridge: the record answers to its column names the way the row did.

methodoffsetGet(mixed $offset): mixed

The migration bridge: one column by its stored name, or null.

methodoffsetSet(mixed $offset, mixed $value): never

Records are read-only; writes go through the repository.

methodoffsetUnset(mixed $offset): never

Records are read-only; writes go through the repository.

Comments final readonly class

public/minn/src/Minn/Content/Comments.php · 245 lines · Open the class →

Reads and writes over the comments table.

new__construct(Minn\Db $db)
methodfind(int $id): ?Minn\Content\CommentRecord

The comment with this id, or null.

methodmeta(int $id, string $key): ?string

One meta value of a comment, or null when it has none.

methodaddMeta(int $id, string $key, string $value): void

Adds a meta row; a second row with the same key is allowed, as the reference allows it.

methodduplicate(int $postId, string $author, string $email, string $content, int $userId): bool

The same words on the same post from the same person, in any status but trash or spam.

methodduplicateId(int $postId, string $author, string $email, string $content, int $userId): ?int

The id of a comment this one would repeat (same post, same author, same words, not spam or trash), or null.

methodlastCommentTime(int $userId, string $address, string $email, string $sinceGmt): ?int

When the commenter last commented since the GMT time given, as a Unix time: matched by user id when signed in, else by address, or by email either way. Null when they have not.

methodpendingCounts(array $postIds): array

Held comments per post, for the ids given (a post without any reads 0).

methodpreviouslyApproved(string $author, string $email): bool

True when this name and email already have an approved comment.

methodinsert(array $columns): int

Inserts a row from column => value pairs and returns the new id.

@param array<string, mixed> $columns
methodupdate(int $id, array $columns): void

Sets the given columns on one comment.

@param array<string, mixed> $columns
methodfieldLengths(): array

How long each comment form field may be, read from the table as it is: a character column allows its declared length, a text column ten bytes fewer than it holds. A column the schema does not describe keeps the stock table's length.

@return array<string, int>
methodidsOf(int $postId): array

The ids of every comment on a post, whatever its status, oldest first. @return list<int>

@return list<int>
methodstatusesOf(int $postId): array

Every comment on a post as id => status, which the trash keeps to give back. @return array<int, string>

@return array<int, string>
methodsetStatusOf(array $ids, string $status): void

Sets one status on the given comments. @param list<int> $ids

@param list<int> $ids
methodorphanReplies(int $id, int $parent): void

A deleted comment's replies move up to its parent.

methoddelete(int $id): void

Removes the comment and its meta; the post's count is the caller's to recount.

methodrecount(int $postId): void

Recomputes a post's stored comment_count (approved comments only).

staticstatusOf(string $approved): string

DB approval token to the wp/v2 status string.

statictokensFor(string $status): ?array

wp/v2 status parameter to DB token(s); null for an unknown status.

@return list<string>|null
staticchangedColumns(array $data, array $current): array

The columns an update really changes, compared as strings against the stored row; the approval shorthands hold/approve become the stored 0/1.

@param array<string, mixed> $data
@param array<string, mixed> $current
@return array<string, mixed>

ContentScan final class

public/minn/src/Minn/Content/ContentScan.php · 196 lines · Open the class →

What a site's stored content asks of the engine: shortcodes, block names, extra tables, extra post types. Preflight lights the findings; the scan itself is pure string and list work so a suite can pin it without a database.

constCORE_TABLES = array ( 0 => 'commentmeta', 1 => 'comments', 2 => 'links', 3 => 'options', 4 => 'postmeta', 5 => 'posts', 6 => 'term_relationships', 7 => 'term_taxonomy', 8 => 'termmeta', 9 => 'terms', 10 => 'usermeta', 11 => 'users', )

Tables the engine already speaks. Anything else is plugin data.

constCORE_TYPES = array ( 0 => 'post', 1 => 'page', 2 => 'attachment', 3 => 'revision', 4 => 'nav_menu_item', 5 => 'custom_css', 6 => 'customize_changeset', 7 => 'oembed_cache', 8 => 'user_request', 9 => 'wp_block', 10 => 'wp_template', 11 => 'wp_template_part', 12 => 'wp_global_styles', 13 => 'wp_navigation', 14 => 'wp_font_family', 15 => 'wp_font_face', )

Built-in types the engine stores. Public ones are in data/types.json; the rest are silent core types that still occupy the posts table.

constCONTENT_TYPES = array ( 0 => 'post', 1 => 'page', 2 => 'wp_block', 3 => 'wp_template', 4 => 'wp_template_part', 5 => 'wp_navigation', )

post_content that a visitor (or a theme template) can actually see.

staticshortcodes(string $content): array

Opening shortcode tags in $content. Escaped [[tag]] is skipped; closers [/tag] never match because a tag name starts with a letter.

@return array<string, int> tag => occurrences
staticblocks(string $content): array

Block names in $content, namespace included. A delimiter without a namespace is core/, matching the parser.

@return array<string, int> name => occurrences
staticextraTables(array $tables, string $prefix): array

The tables under the prefix that are not the reference's own.

@param list<string> $tables names from SHOW TABLES
@return list<string> bare names after stripping the prefix
statictableFamilies(array $bare): array

Group extra tables by the first underscore segment. Wordfence's tables are wfHits and wfls_* with no shared underscore prefix, so those two stems collapse to one family.

@param list<string> $bare
@return list<string>
staticextraTypes(array $types): array

The post types in use that the reference does not register itself.

@param list<string> $types
@return list<string>
staticthirdParty(array $named): array

The block names that are not core/*, with their counts.

@param array<string, int> $named
@return array<string, int>
staticlisted(array $names, int $cap = 40): string

Names sorted and comma-separated, cut with a count past the cap.

@param list<string> $names
insidewalk()

Emoji final class

public/minn/src/Minn/Content/Emoji.php · 68 lines · Open the class →

Emoji as the reference's mail and feeds carry them, from the list the reference ships (data/emoji.json, read from it): encode() turns each emoji character into its entity; staticize() turns each listed sequence in the text (outside tags, and outside code and pre) into a CDN image, longest sequence first. Text that already holds an entity is not encoded first; once anything matched, the variation selectors left over are dropped.

staticencode(string $text): string

Each emoji character in the text as its hexadecimal entity.

staticstaticize(string $text, string $baseUrl, string $extension): string

The text with its emoji as images under $baseUrl (each file named for its code points, $extension after).

insidereplaceInText(), list()

Excerpt final class

public/minn/src/Minn/Content/Excerpt.php · 90 lines · Open the class →

The reference's generated excerpt, as captured from probe posts:

- Only text-bearing blocks contribute. Containers (group, columns, column, media-text) pass their allowed children through; anything not on the list vanishes with everything inside it (code, details, buttons, images, galleries, covers, dynamic blocks, and the nested list-item, so a modern list contributes nothing while a classic one does). - The text stops at the first <!--more--> (in a feed it runs on). - Block-level tags read as spaces; inline tags (and <br>) read as nothing. - Whitespace collapses, 55 words, an ellipsis when cut, wrapped in <p>, texturized (a feed texturizes first, so its inline code stays raw). A hand-written excerpt skips the block filter.

staticrender(Minn\Content\PostRecord $post): string

The more tag ends a listing's excerpt but not a feed's, and a feed texturizes before the tags go (so inline code keeps straight quotes) where a listing texturizes the finished text.

insidesource(), words(), recordRendered(), allowedMarkup()

Inventory final readonly class

public/minn/src/Minn/Content/Inventory.php · 253 lines · Open the class →

Plugins, themes, must-use plugins, and drop-ins as they sit on disk. The shapes match `wp plugin list` / `wp theme list` captured from the reference: name is the directory (or the drop-in filename), title and version come from the file headers, status from the options.

constKNOWN_DROPINS = array ( 'advanced-cache.php' => array ( 0 => 'Advanced caching plugin.', 1 => 'WP_CACHE', ), 'db.php' => array ( 0 => 'Custom database class.', 1 => true, ), 'db-error.php' => array ( 0 => 'Custom database error message.', 1 => true, ), 'install.php' => array ( 0 => 'Custom installation script.', 1 => true, ), 'maintenance.php' => array ( 0 => 'Custom maintenance message.', 1 => true, ), 'object-cache.php' => array ( 0 => 'External object cache.', 1 => true, ), 'php-error.php' => array ( 0 => 'Custom PHP error message.', 1 => true, ), 'fatal-error-handler.php' => array ( 0 => 'Custom PHP fatal error handler.', 1 => true, ), )

The drop-ins a single site recognises, in the reference's order: the file => its description and the constant that must be true for it to load (true when nothing gates it).

new__construct(string $contentDir, Minn\Content\Site $site)
methodplugins(): array

Regular plugins, then must-use, then drop-ins, each group sorted by name.

@return list<array<string, mixed>>
methodmustUse(): array

The mu-plugins folder's PHP files, each with its header.

@return list<array<string, mixed>>
methoddropins(): array

The drop-in files present in wp-content.

@return list<array<string, mixed>>
methodthemes(): array

Every theme folder with its style.css header.

@return list<array<string, mixed>>
methodpluginFiles(): array

Every regular plugin's main file: relative "dir/file.php" (or "file.php" for a single-file plugin) to its absolute path.

@return array<string, string>
insideregularPlugins(), mainPluginFile(), item()

MediaShortcodes final class

public/minn/src/Minn/Content/MediaShortcodes.php · 187 lines · Open the class →

The [video] and [audio] shortcodes as the reference prints them: a plugin may take either over first (wp_*_shortcode_override, handed the shortcode's own attributes and its instance number), then the source by src or by each format's attribute (a file of another kind is only a link; YouTube and Vimeo addresses are video of their own; with none, the post's first attached video or audio), the player's attributes (a video no wider than the content, its height in proportion; the flags bare), each source numbered by the instance, a fallback link, and the class, library and final HTML through their filters.

staticvideo(array|string $attr, string $content): ?string

[video]. @param array<string, mixed>|string $attr

@param array<string, mixed>|string $attr
staticaudio(array|string $attr, string $content): ?string

[audio]. @param array<string, mixed>|string $attr

@param array<string, mixed>|string $attr
staticattached(string $type, mixed $post): array

get_attached_media: the post's attachments of a kind ("video", "audio", a MIME type), as the reference's children query finds them.

insidesources(), anyOwnFormat(), sourceTags(), attributes(), fitted(), library(), fallback(), postId(), instance()

MoreTag final class

public/minn/src/Minn/Content/MoreTag.php · 20 lines · Open the class →

The `<!--more-->` marker that splits a post into the part a listing shows and the part only the single view does. The marker may carry its own link text; a second marker further down is ordinary content and stays where it is, as does the `<!--noteaser-->` flag that follows some of them.

staticsplit(string $content): array

The content on either side of a more tag, with the tag's own text.

@return array{main: string, extended: string, more_text: string}

NoteMarkers final class

public/minn/src/Minn/Content/NoteMarkers.php · 34 lines · Open the class →

The markers the block editor's notes leave in content: a mark element whose class list holds "wp-note". A rendered block loses the marker and keeps what it wrapped, nested markers too, an unclosed one losing only its opening tag; any other mark stays whole (probe comment-helpers).

staticstrip(string $html): string

Markup with every note marker's tags taken out.

insideisNote()

ObjectLabels final class

public/minn/src/Minn/Content/ObjectLabels.php · 24 lines · Open the class →

A post type's or taxonomy's labels from its own and the defaults (_get_custom_object_labels, probe post-more): the name from its label when unset, the singular from the name, the admin bar from the singular (else the object's name), the menu from the name, all items from the menu, archives from all items; then the defaults (the caller picks the flat or the hierarchical set) under those, in the defaults' order.

staticbuild(array $labels, ?string $label, string $name, array $defaults): array

The labels, the object's own filled in by the rules above and laid over the defaults.

@param array<string, mixed> $labels the object's own
@param array<string, mixed> $defaults key => the default for this object
@return array<string, mixed>

Page final readonly class

public/minn/src/Minn/Content/Page.php · 43 lines · Open the class →

One page of a listing: the rows on it and how many rows the whole listing has, which is what pagination is counted from.

new__construct(array $posts, int $total)
@param list<PostRecord> $posts
propreadonly array $posts
propreadonly int $total
staticempty(): self

No rows and a total of zero.

methodisEmpty(): bool

Whether the page holds no rows.

methodcount(): int

How many rows are on this page.

methodids(): array

The ids of the rows on this page.

@return list<int>
methodtotalPages(int $perPage): int

How many pages the whole listing makes at this page size.

PasswordGate final class

public/minn/src/Minn/Content/PasswordGate.php · 75 lines · Open the class →

A password-protected post on the front end: its body is the password form, its excerpt a fixed sentence, its title prefixed, exactly as the reference shows them to a reader who has not entered the password.

constEXCERPT = 'There is no excerpt because this is a protected post.'
staticis(Minn\Content\PostRecord $post): bool

True while the post has a password the reader's cookie does not match.

staticcookieUnlocks(string $password, string $cookie): bool

Whether a wp-postpass cookie opens a post with this password. Only a hash in the setting the reference writes ("$P$B") is tried, as the reference tries it: a cookie naming its own work factor could otherwise make one request hash up to 2^30 times.

statictitle(Minn\Content\PostRecord $post): string

"Protected: " for a password, "Private: " for a private post, as the reference prefixes titles.

staticform(Minn\Content\PostRecord $post, string $siteUrl, string $permalink, string $invalid = ''): string

The form, with the reference's stray closing p after the hidden field; with the message a wrong password earns ($invalid, see invalid()) above the field and named by it.

staticformForReader(Minn\Content\PostRecord $post, string $siteUrl, string $permalink): string

The form as the current reader sees it: with the wrong-password message when their own form sent them back.

staticinvalid(string $permalink, string $referer, string $cookie, Closure $post): string

The message over the form for a reader the post's own page sent back with a password cookie set (the referer is exactly its address), as plugins word it (the_password_form_incorrect_password); '' for anyone else. The cookie is not checked: the form only shows while it fails. $post gives the post the filter is handed, asked for only then.

@param \Closure(): mixed $post

PersonalData final class

public/minn/src/Minn/Content/PersonalData.php · 36 lines · Open the class →

Personal data as the privacy tools hand it over (probe privacy): an exported item is a labelled group of name and value rows in which a row without a value is left out, and an export file is old once it was last written before the cut-off (the folder's index.php and hidden files are never old).

staticitem(string $groupId, string $groupLabel, string $groupDescription, string $itemId, array $rows): array

One exported item, its rows in the order given, a row whose value is empty left out.

@param array<string, mixed> $rows name => value
@return array{group_id: string, group_label: string, group_description: string, item_id: string, data: list<array{name: string, value: mixed}>}
staticexpiredExports(string $dir, int $before): array

The export files in $dir last written before $before.

@return list<string>

Playlist final class

public/minn/src/Minn/Content/Playlist.php · 25 lines · Open the class →

The [playlist] shortcode's markup as the reference prints it (probe plugin-queue5): the player for audio (with the current-item box) or for video (with its height), the next and previous controls, a noscript list of links to the files, and the tracks as JSON for wp-playlist; and the two Underscore templates the script draws them with (data/playlist-templates.html).

staticmarkup(string $type, string $style, int $width, int $height, array $links, string $json): string

The player and its data.

@param list<string> $links one link per track, for the noscript list
statictemplates(string $quoted): string

The two templates, each title wrapped in the quoting format given (its %s takes the title placeholder).

PluginState final readonly class

public/minn/src/Minn/Content/PluginState.php · 72 lines · Open the class →

Switching plugins on and off: a WordPress plugin file goes through the reference's activation and deactivation (PluginActivation: requirements, the plugin loaded, its hooks, the sorted active_plugins list); a Minn extension joins or leaves minn_active_extensions, and deactivating one also releases, silently, the plugin files it stood in for. The REST toggle and the CLI verbs share this so they cannot drift; both run with the runtime booted.

new__construct(Minn\Content\Site $site, Minn\Content\Inventory $inventory, Minn\Extension\Loader $extensions)
methodfind(string $slug): Minn\Extension\Manifest|string|null

The relative "dir/file.php" of a WordPress plugin, else the extension for a slug, else null. A folder carrying both records in active_plugins like the reference does (the loader counts its own folder there as active); only a pure extension uses the engine's list.

methodisActive(Minn\Extension\Manifest|string $plugin): bool

Whether a plugin, by file, or a Minn extension is active.

methodactivate(Minn\Extension\Manifest|string $plugin): ?Minn\Runtime\Refusal

Records a plugin or an extension as active, in the option each kind uses; null, or why a plugin was refused (unexpected_output leaves it active, as the reference does).

methoddeactivate(Minn\Extension\Manifest|string $plugin): void

Records a plugin, by file, or a Minn extension as inactive; an extension also deactivates the plugins it replaced.

insideownWithout()

PostClasses final class

public/minn/src/Minn/Content/PostClasses.php · 52 lines · Open the class →

The class list a post carries on its article element, in the reference's order: caller extras, identity, type, status, format, password state, thumbnail, sticky, hentry, then one class per term of every public taxonomy (post_tag reads as "tag-"; the format's term counts too, as post_format-post-format-aside; probe post-template-remaining).

staticbuild(array $post, array $extra, ?string $format, bool $thumbnail, bool $sticky, bool $passwordRequired, bool $hasPassword, array $terms): array

The class list a post carries on its article element, in the reference's order.

@param list<string> $extra
@param list<array{taxonomy: string, slug: string, term_id: int}> $terms
@return list<string>
statichtmlClass(string $value): string

The reference keeps letters, digits, hyphens and underscores in a class name.

PostRecord final readonly class

public/minn/src/Minn/Content/PostRecord.php · 156 lines · implements ArrayAccess · Open the class →

One row of the posts table, read by name. The columns keep their WordPress spelling on the way in (row()) and get plain names here: $post->title, $post->slug, $post->status. A record is built from a row and can hand the row back, so a writer or a REST shaper that still works in columns is not disturbed.

Array access is the migration bridge: code that still reads $post['post_title'] keeps working while it is moved over. New code reads the properties. The style suite counts the bracket reads down.

propreadonly int $id
propreadonly int $authorId
propreadonly string $date
propreadonly string $dateGmt
propreadonly string $content
propreadonly string $title
propreadonly string $excerpt
propreadonly string $status
propreadonly string $commentStatus
propreadonly string $pingStatus
propreadonly string $password
propreadonly string $slug
propreadonly string $modified
propreadonly string $modifiedGmt
propreadonly int $parentId
propreadonly string $guid
propreadonly int $menuOrder
propreadonly string $type
propreadonly string $mimeType
propreadonly int $commentCount
staticfromRow(array $row): self

A record from a posts row; a missing column reads as empty.

@param array<string, mixed> $row a posts-table row, joined columns welcome
staticfromRows(array $rows): array

A record for every row, in order.

@param list<array<string, mixed>> $rows @return list<self>
methodrow(): array

The stored row, for the writers and shapers that still spell columns.

The row as the table holds it, joined columns and all.

methodstatus(): ?Minn\Content\PostStatus

The status as an enum case, or null for a status only a plugin knows.

methodisPublished(): bool

Published, and so public.

methodisLive(): bool

Publish, future or private: it counts, it links, it is not a draft.

methodisTrashed(): bool

In the trash.

methodisProtected(): bool

Behind a password.

methodisPage(): bool

A page.

methodisAttachment(): bool

An attachment.

methodcolumn(string $name): mixed

One stored column by its database name, or null.

A joined column, or any column by its table name.

methodoffsetExists(mixed $offset): bool

The migration bridge: the record answers to its column names the way the row did.

methodoffsetGet(mixed $offset): mixed

The migration bridge: one column by its stored name, or null.

methodoffsetSet(mixed $offset, mixed $value): never

Records are read-only; writes go through the repository.

methodoffsetUnset(mixed $offset): never

Records are read-only; writes go through the repository.

PostSlugs final readonly class

public/minn/src/Minn/Content/PostSlugs.php · 75 lines · Open the class →

Which slug a live post may take beside the others, as the reference settles it (probe insert-defaults). A flat type's slugs are its own: a post, a page and a plugin's type may share one. A hierarchical type's are its own and its attachments' under the same parent. An attachment's are every post's. Never free: the feed names and embed; for a hierarchical type, a number or a page number (2, page2); for posts, a number a date archive would answer to: any number when the permalink structure begins with the post name, a month (below 13) when the post name follows the year, a day (below 32) when it follows the month. A post keeps a number it already has. A taken slug gets the first free -2, -3 form, cut (and stripped of a trailing hyphen) to fit the column's 200 characters. The slug is taken as given, not sanitized.

new__construct(Minn\Db $db, Minn\Content\Site $site)
methodunique(string $slug, int $excludeId, string $type, int $parent, ?Closure $bad = NULL): string

The slug, or its first free numbered form.

@param Closure(string): bool|null $bad a plugin's say over the slug asked for (the bad-slug filters), asked last
insidetaken(), reserved(), hierarchical()

PostStatus enum

public/minn/src/Minn/Content/PostStatus.php · 42 lines · Open the class →

The statuses a post row can hold; the value is the column's own spelling.

Cases: Publish = 'publish', Draft = 'draft', Pending = 'pending', Private = 'private', Future = 'future', Trash = 'trash', Inherit = 'inherit', AutoDraft = 'auto-draft'

staticof(Minn\Content\PostRecord|array $row): ?self

A row's status, or null for a value no enum case spells (a plugin's own status).

methodisLive(): bool

Publish, future and private are live: they count, they link, they are not drafts.

methodisPublic(): bool

Whether the status shows the post to anonymous readers.

staticvalues(array $statuses): array

The stored strings of these statuses.

@param list<self> $statuses @return list<string> the column values, for a query

PostWriter final readonly class

public/minn/src/Minn/Content/PostWriter.php · 503 lines · Open the class →

Every write to the posts table and its satellites: rows, meta, term links with the published counts the reference trusts on read, sticky and format side effects, and revision snapshots.

constFLOATING = array ( 0 => 'draft', 1 => 'pending', 2 => 'auto-draft', )

The statuses whose post may have a floating date (no GMT date yet).

constZERO_DATE = '0000-00-00 00:00:00'
new__construct(Minn\Db $db, Minn\Content\Posts $posts, Minn\Content\Site $site)
methodhasTerm(string $taxonomy, int $termId): bool

Whether a term of the taxonomy has the id.

methodinsert(array $columns): int

Inserts a posts row from column => value pairs and returns the new id.

@param array<string, mixed> $columns
methodupdate(int $id, array $columns): void

Sets the given columns on one post; nothing happens for none.

@param array<string, mixed> $columns
methodsetStatus(int $id, string $status): void

Changes one post's status.

methodsetType(int $id, string $type): bool

Moves a post to another type, leaving it alone when it is already there.

methodsetMeta(int $id, string $key, string $value): void

Sets one meta value, inserting the row when the key is new.

staticfloating(Minn\Content\PostRecord $post): bool

Whether a post's date floats: never given one, it is still a draft or pending with no GMT date.

methodaddMeta(int $id, string $key, string $value): void

Adds a meta row even when the key already has one (a non-unique key).

methodrememberOld(int $id, string $key, string $was, string $now): void

Keeps a published post's previous slug or date on record (_wp_old_slug, _wp_old_date) so its old address still finds it: the value it moves to stops being an old one, the value it leaves becomes one, once.

methodensureCategory(int $id): void

Gives a post the site's default category when it has none, as every save of a post does in the reference.

methodtrash(Minn\Content\PostRecord $post): void

The row of a post moving to the trash, as the reference writes it: the slug gains __trashed, the modified time moves, a floating draft's date settles, a post keeps a category, and the published counts follow. What the trash keeps for the way back (the status, the time, the slug it had) is meta the caller adds, and the revision comes from the save.

methoddeleteMeta(int $id, string $key): void

Removes every meta row with this key from a post.

methoduniqueSlug(string $desired, int $excludeId, string $type = 'post', int $parent = 0): string

A free slug for the desired text, sanitized first, in the type's scope (see PostSlugs).

methodslugs(): Minn\Content\PostSlugs

The rules a live post's slug is settled by.

methodsetTerms(int $id, string $taxonomy, array $termIds): void

Replaces a post's links in one taxonomy and refreshes that taxonomy's counts.

methodtaxonomiesOf(int $id): array

The taxonomies a post has terms in.

@return list<string> the distinct taxonomies a post has links in
methodrecountTaxonomiesOf(int $id): void

Recounts every term the post is in, after a status change.

methodrecount(string $taxonomy): void

term_taxonomy.count is stored and trusted on read, so every status change refreshes it.

methodisSticky(int $id): bool

Whether the post is in the sticky_posts option.

methodstick(int $id): void

Puts a post on the sticky list, then post_stuck when it was not on it (probe rest-sticky).

methodunstick(int $id): void

Takes a post off the sticky list, then post_unstuck when it was on it (probe rest-sticky).

methodsetFormat(int $id, string $format): void

Assigns (or clears) the post-format term, creating it on first use.

methodapplyExtendedFields(int $id, array $body, string $type): void

The side effects shared by create and update: sticky, format, featured media.

methodapplyCoreMeta(int $id, array $body, string $type): void

The meta core registers, written directly when no plugins are loaded: footnotes, and a pattern's sync status.

methodapplyTerms(int $id, array $body): void

Assigns categories, tags, and pattern categories from a write body, replacing existing links.

staticrequestedTerms(array $body, ?array $map = NULL): array

The terms a REST body names, by taxonomy: categories, tags, and pattern categories, each only when the body has the field.

@param array<string, mixed> $body
@param array<string, string>|null $map REST field => taxonomy; the core taxonomies when null
@return array<string, list<int>>
methodmaybeSaveRevision(int $id, int $userId): void

Snapshots the post's NEW state as a revision exactly when the reference would: compared against the latest revision, identical content-bearing fields add nothing, and the first update always snapshots.

methodrevisionColumns(int $id, int $userId): ?array

The row of the revision a post's current state calls for, or null when it calls for none (an unrevisioned type, or nothing changed since the latest revision).

@return array<string, mixed>|null
methodinsertRevision(array $columns): int

Writes a revision row and gives it its guid; returns its id. @param array<string, mixed> $columns

@param array<string, mixed> $columns
methodreparentChildren(int $id, int $parent, array $types): void

Moves a deleted post's children of the given types to another parent, the way the reference keeps pages and attachments attached. @param list<string> $types

@param list<string> $types
methodreassignAuthor(int $from, int $to): void

Moves every post of one author to another.

methoddb(): Minn\Db

The database door this writer writes through, for a caller wrapping several of its verbs in one transaction.

methoddestroy(int $id): void

Hard-deletes a post with its revisions and its meta.

insidefit(), announceSticky(), saveSticky(), revisionsToKeep(), pruneRevisions()

Posts final readonly class

public/minn/src/Minn/Content/Posts.php · 553 lines · Open the class →

Reads over the posts table. A post comes back as a PostRecord; a listing is the main query's (WP_Query); rendering and escaping happen elsewhere.

new__construct(Minn\Db $db)
methodfind(int $id): ?Minn\Content\PostRecord

The post with this id, or null.

methodprime(array $ids, array $taxonomies = array ( )): void

Reads a list's posts, their meta and their terms in the given taxonomies in one query each, as update_post_caches does, so the list's rendering reads none of them again.

@param list<int> $ids
@param list<string> $taxonomies
methodrememberRows(array $rows): void

Keeps rows a query already read (WP_Query's whole posts), so priming those posts reads them no second time.

@param list<array<string, mixed>> $rows
methodprimeRows(array $ids): void

Reads the rows of the posts not read yet, in one query. @param list<int> $ids

@param list<int> $ids
methodprimeMeta(array $ids): void

Reads the meta of the posts whose meta is not read yet, in one query (update_post_meta_cache). @param list<int> $ids

@param list<int> $ids
methodprimeTerms(array $ids, array $taxonomies): void

Reads the posts' terms in the given taxonomies, in one query, for the posts some of whose taxonomies are not read yet.

@param list<int> $ids
@param list<string> $taxonomies
methodfindByName(string $name, array $types): ?Minn\Content\PostRecord

The post with this slug among the given types; published only unless asked otherwise.

@param list<string> $types
methodfindByNameAnyStatus(string $name, array $types): ?Minn\Content\PostRecord

The post with this slug among the given types in any status but trash, or null. @param list<string> $types

@param list<string> $types
methodbyOldSlug(string $slug, array $types): ?Minn\Content\PostRecord

The post that once answered to the slug: every former slug stays in `_wp_old_slug` meta, and the reference redirects it to the current link whatever the post's status (a draft or trashed post goes to its `?p=` form).

@param list<string> $types
methodpageByPath(array $segments): ?Minn\Content\PostRecord

Walks a page hierarchy: ["sample-page", "docs"] finds the page named docs whose parent is named sample-page at the root.

@param list<string> $segments
methodpageByPathAnyStatus(array $segments): ?Minn\Content\PostRecord

The page at a slug path in any status but trash, or null. @param list<string> $segments

@param list<string> $segments
methodbyTypedPath(array $segments, array $types): ?Minn\Content\PostRecord

The post at a slug path among the given types (each segment's parent the one before it), in any status but trash; at the last segment the first type wins over the others, as the reference prefers the type it was asked for over the attachment it also looks at.

@param list<string> $segments
@param list<string> $types
methodpathOf(Minn\Content\PostRecord $page): string

The slash-joined ancestry of a page: "sample-page/docs".

methodguess(string $prefix, array $types): ?Minn\Content\PostRecord

The published post whose name starts with the given text, among the given types, as the reference guesses a destination for a missing URL: one prefix query and no order of its own, so the database's plan for it decides which match comes first (on a small table its name index, alphabetical; on a large one its type and date index, oldest first), the same on both stacks over one database.

@param list<string> $types
methodhasPublished(string $type): bool

Whether any post of a type is published.

methodidsOfType(string $type): array

Every post id of a type, whatever its status, as stored (strings), in the table's order. @return list<string>

@return list<string>
methodmeta(int $postId, string $key): ?string

One meta value of a post, or null when it has none.

methodterms(int $postId, string $taxonomy): array

The post's terms in one taxonomy, as [term_id, slug] pairs.

@return list<array{0: int, 1: string}> term id and slug pairs, by name
methodrevisionCount(int $postId): int

How many revisions the post has.

methodprimeRevisions(array $ids): void

Reads many posts' revision counts and latest revisions in one query, for a list that links to each post's history (a REST list).

@param list<int> $ids
methodlatestRevisionId(int $postId): int

The latest revision id, an autosave counting as one, newest by date then id (probe rest-latest-revision), or 0.

methodhasNewerAutosave(int $postId, string $modifiedGmt): bool

Whether a live post carries an autosave newer than its saved state.

methodnewest(array $types, string $column): ?string

The newest value of a date column (post_date, post_modified, or their GMT twins) among published posts of the types given, for get_lastpostdate and get_lastpostmodified.

@param list<string> $types
methoddaysWithPosts(int $year, int $month, string $type): array

The days of one month a published post of a type was dated on, ascending.

@return list<int>
methodarchiveBuckets(string $granularity, string $type, string $order, int $limit, string $where = '', string $join = ''): array

The periods holding a published post of a type, newest first by default: one row per month, year, day, or week, with the count and the earliest post date inside it. A WHERE and JOIN given (what wp_get_archives' getarchives_where and getarchives_join answered) are used as they are.

@return list<array{year: int, month: int, day: int, week: int, count: int, first: string}>
methodadjacentClauses(Minn\Content\PostRecord $post, string $direction, string $taxonomy, array $sameTerms, array $excluded, string $private, int $reader): array

The JOIN, WHERE and ORDER BY of the post before ($direction "previous") or after a post, written as the reference writes them for get_adjacent_post (probe filter-inputs), for its filters to edit: kept to $sameTerms in $taxonomy when there are some, out of the $excluded terms, and published, or private too ($private "all"), or the reader's own private ones too ($private "own").

@param list<int> $sameTerms
@param list<int> $excluded
@return array{0: string, 1: string, 2: string}
methodadjacentId(string $join, string $where, string $sort): int

The post the clauses find (as their filters left them), or 0.

methodarchiveList(string $type, string $orderBy, string $order, int $limit, string $where = '', string $join = ''): array

Published posts of a type for a post-by-post archive, by date or by title; a WHERE and JOIN given are used as they are (archiveBuckets).

@return list<PostRecord>
methodmonthBefore(int $year, int $month, string $type): ?array

The nearest month before one with a published post of a type, as [year, month], or null.

methodmonthAfter(int $year, int $month, string $type): ?array

The nearest month after one with a published post of a type, as [year, month], or null.

methodnewestAutosave(int $postId, int $userId = 0): ?Minn\Content\PostRecord

The newest autosave of a post (by date, then id), by one author or, given 0, any (probe revision-more); null when there is none.

methodfirstCategorySlug(int $postId): ?string

The slug of the post's first category, or null.

insiderecord(), ids(), byName(), byPath(), readTerms(), revisions(), readRevisions(), weekMode(), monthBeside()

Reader final class

public/minn/src/Minn/Content/Reader.php · 65 lines · Open the class →

Who is reading this request: their user id, whether they may read private content, whether they may edit a given post, and the post-password cookie they carry. Built once per surface by the engine, carried by the request's context, and consulted by the resolver, the queries, and the renderers.

new__construct(int $userId, bool $readsPrivatePosts, bool $readsPrivatePages, Closure $canEditPost, string $postPassword, string $sessionToken = '', array $roles = array ( ))
@param Closure(int): bool $canEditPost
propreadonly int $userId
propreadonly bool $readsPrivatePosts
propreadonly bool $readsPrivatePages
propreadonly string $postPassword
propreadonly string $sessionToken
propreadonly array $roles
staticanonymous(string $postPassword = ''): self

The reader who is nobody: no session, no private posts, only the post password they typed.

staticforUser(int $userId, Minn\Auth\Capabilities $capabilities, string $postPassword = '', string $sessionToken = ''): self

The reader a signed-in user is, asked of the capability engine: what they may read privately, what they may edit, and the roles they hold. User 0 is nobody, whatever the capabilities say.

staticcurrent(): self

The reader of the request being answered. The runtime holds it (the context it was built with carries it), so there is one holder and a request cannot see the reader of the one before it. Without a runtime, on the command line and in unit tests, nobody is reading.

methodloggedIn(): bool

Whether the reader has a session.

methodcanEdit(int $postId): bool

Whether this reader may edit a given post.

Revisions final readonly class

public/minn/src/Minn/Content/Revisions.php · 95 lines · Open the class →

Revision rows: the plain snapshots and the per-author autosave slots.

new__construct(Minn\Db $db, Minn\Content\PostWriter $writer, Minn\Content\Site $site)
methodrevisionsOf(int $parentId): array

The revisions, or the autosaves, of a post, newest first, as rows.

@return list<array> newest first
methodallOf(int $parentId): array

Every revision of a post, its autosaves among them, newest first by date then id, as rows: what the revisions route lists (probe rest-latest-revision).

methodautosavesOf(int $parentId): array

The autosaves of a post, newest first, as rows.

methodsaveAutosave(int $parentId, int $userId, string $title, string $content, string $excerpt): int

One autosave slot per author: updated in place when it exists.

insidenamed()

RowCache final class

public/minn/src/Minn/Content/RowCache.php · 100 lines · Open the class →

Rows read once a request: posts, their terms, users, and every object's meta, as the reference keeps them in its object cache, so a loop or a REST list does not read the same row once per use. A write to a table drops everything kept from it (Db::wrote, through the engine's own door or the facade's wpdb), as does a rolled-back transaction, so a read after a write finds the row as it now is. Nothing outlives the request.

staticremember(string $kind, string|int $key, Closure $load): mixed

The kept value of a kind for a key, read through $load the first time.

statichas(string $kind, string|int $key): bool

Whether a value is kept.

staticpeek(string $kind, string|int $key): mixed

A kept value, or null when there is none.

staticput(string $kind, string|int $key, mixed $value): void

Keeps a value read elsewhere (a batch read for a whole list).

staticwatch(string $kind, string $table): void

Has a kind named at run time (a plugin's meta type, whose table only the registration knows) dropped whenever that table is written, as the kinds above are by theirs.

staticforget(string $kind, string|int|null $key = NULL): void

Drops one kept value, or every one of a kind.

staticflush(): void

Drops everything kept.

insidelisten()

Site final readonly class

public/minn/src/Minn/Content/Site.php · 114 lines · Open the class →

Site-wide options and the site's clock.

new__construct(Minn\Db $db)
methodoption(string $name): ?string

One option's raw value, or null when it is unset.

methodsetOption(string $name, string $value, ?string $autoload = NULL): void

Writes an option, inserting it as autoloaded when it does not exist; the option store hears of it. An $autoload given is written too, so a cache the engine reads on demand (autoload "off") stays out of the list every request loads.

methoddeleteOption(string $name): void

Removes an option row; the option store hears of it.

methoddefaultDiscussion(string $type, string $kind): string

A new post's comment or ping status, as the reference picks it: a post follows the site's two defaults, an attachment follows the comment default and never takes pings, a page and every other type start closed; with plugins loaded, get_default_comment_status() decides (support a plugin removed, the filter). $kind is "comment" or "pingback".

methodtimezone(): DateTimeZone

The site's timezone (wp_timezone): the zone timezone_string names, else gmt_offset as a fixed offset. With a zone set, the stored gmt_offset is not read at all, as the reference does not read it.

methodgmtOffset(): int

The site's offset from UTC in seconds now: the zone's, daylight saving included, or gmt_offset's.

methodlocalNow(): string

Now in the site's local time, MySQL format.

methodlocalDate(string $input): array

A site-local datetime from a write body ("2031-01-01T00:00:00") to the pair of columns it fills, plus its timestamp.

@return array{0: string, 1: string, 2: int} local, gmt, timestamp
methodgmtDate(string $input): array

A GMT datetime from a write body (date_gmt) to the same pair of columns, its local half in the site's zone at that date.

@return array{0: string, 1: string, 2: int} local, gmt, timestamp

SiteIcon final readonly class

public/minn/src/Minn/Content/SiteIcon.php · 63 lines · Open the class →

The site icon: the attachment the site_icon option names, as the file under uploads it points at. Read by the favicon route, the head links, and the admin app's sidebar.

new__construct(Minn\Content\Site $site, Minn\Content\Posts $posts, Minn\Front\Permalinks $permalinks)
methodfile(): ?string

The icon's path under uploads, or null when the site has none.

methodurlAt(int $size): string

The URL of the icon's smallest generated size that covers a square of $size pixels, or of the original when no size does; empty without an icon.

methodurl(): string

The icon's URL, empty when the site has none.

methodlinks(): string

The head links a site with an icon prints (wp_site_icon): the 32 and 192 pixel icons, the Apple touch icon, and the tile image; empty without one.

Slug final class

public/minn/src/Minn/Content/Slug.php · 81 lines · Open the class →

staticsanitize(string $text, string $locale = ''): string

A title as the reference's sanitize_title makes a slug of it when saving, without the filters (the runtime's sanitize_title adds those): accents spelled plainly, then the dash form, non-ASCII percent-encoded up to 200 bytes.

staticuriEncode(string $text, int $length, Closure $ascii): string

Non-ASCII characters as lower-case percent escapes, a whole character at a time, stopping before one would take the text past $length bytes (0 for no limit); each ASCII character goes through $ascii (as it is, or rawurlencode).

@param Closure(string): string $ascii
statictruncate(string $slug, int $length): string

A slug cut to a byte length without splitting a multibyte character or leaving a dash hanging off the end.

staticdashes(string $title, bool $forSave, Closure $utf8Encode): string

The reference's dash form of a title: tags and marks stripped, spaces to dashes, and the save-time rules when saving.

TagBalancer final class

public/minn/src/Minn/Content/TagBalancer.php · 82 lines · Open the class →

Closes what markup leaves open and drops what it closes without opening, as the reference's force_balance_tags does, from its observed rules (contracts/runtime.md "The content filters"): tag names are lowercased; a void element is written self-closed (bare as <br />, with attributes as <img src="x"/>, one already self-closed as given); reopening the innermost open element closes it first unless it is one of the ten that may contain themselves; a closer shuts every element opened inside its own; a closer with nothing to close goes; what is still open at the end closes in reverse. Script and style text is left as it is, and a "<" not followed by a name is text.

staticbalance(string $text): string

The markup with every element closed in order and every stray closer gone.

insideclose(), voidAttributes(), rawText()

TermRecord final readonly class

public/minn/src/Minn/Content/TermRecord.php · 88 lines · implements ArrayAccess · Open the class →

One term with its taxonomy row, read by name: $term->name, ->slug, ->taxonomy, ->parentId, ->count. The taxonomy and description are empty when the query that built the record did not select them. Array access is the migration bridge, read-only.

propreadonly int $id
propreadonly string $name
propreadonly string $slug
propreadonly int $taxonomyId
propreadonly string $taxonomy
propreadonly string $description
propreadonly int $parentId
propreadonly int $count
staticfromRow(array $row): self

A record from a joined terms row; a missing column reads as empty.

@param array<string, mixed> $row a terms row joined with term_taxonomy, whatever columns it carries
staticfromRows(array $rows): array

A record for every row, in order.

@param list<array<string, mixed>> $rows @return list<self>
methodrow(): array

The stored row, for the writers and shapers that still spell columns.

methodhasParent(): bool

Whether the term sits under another.

methodcolumn(string $name): mixed

One stored column by its database name, or null.

methodoffsetExists(mixed $offset): bool

The migration bridge: the record answers to its column names the way the row did.

methodoffsetGet(mixed $offset): mixed

The migration bridge: one column by its stored name, or null.

methodoffsetSet(mixed $offset, mixed $value): never

Records are read-only; writes go through the repository.

methodoffsetUnset(mixed $offset): never

Records are read-only; writes go through the repository.

Terms final readonly class

public/minn/src/Minn/Content/Terms.php · 219 lines · Open the class →

new__construct(Minn\Db $db)
methodfindBySlug(string $taxonomy, string $slug): ?Minn\Content\TermRecord

A term joined with its taxonomy row: term_id, name, slug, term_taxonomy_id, parent, count.

methodfind(string $taxonomy, int $termId): ?Minn\Content\TermRecord

One term in a taxonomy, or null.

methodrow(int $termId, string $taxonomy): ?Minn\Content\TermRecord

The row shape the term controllers work with, including term_taxonomy_id.

methodidByName(string $name, string $taxonomy): ?int

The id of the term with exactly this name in a taxonomy, or null.

methoduniqueSlug(string $base, string $taxonomy, int $skipId = 0): string

A slug unique within the taxonomy: base, -2, -3 on collision.

methodcreate(string $name, string $slug, string $taxonomy, string $description, int $parent): int

Inserts a term and its taxonomy row and returns the term id.

methodinsertRow(array $data): int

Inserts a terms row alone (name, slug, term_group), as the reference writes it before the taxonomy row; returns the term id.

@param array{name: string, slug: string, term_group: int} $data
methodaddToTaxonomy(int $termId, string $taxonomy, string $description, int $parent): int

Puts a term in a taxonomy; returns the term_taxonomy id.

methodupdateRow(int $termId, array $data): void

Rewrites a terms row (name, slug, term_group).

@param array{name: string, slug: string, term_group: int} $data
methoddropRows(int $termId, int $ttId): void

Drops a term's rows that a plugin's duplicate check said to give up.

methodrename(int $termId, string $name, string $slug): void

Changes a term's name and slug.

methoddescribe(int $termId, string $taxonomy, string $description, int $parent): void

Changes a term's description and parent within one taxonomy.

methoddelete(Minn\Content\TermRecord $term, bool $hierarchical): void

Reparents children to the grandparent, detaches relationships, drops the rows.

methodpathOf(Minn\Content\TermRecord $term): string

"parent/child" for hierarchical taxonomies, the bare slug otherwise.

insiderecord(), refreshHierarchy(), keepsHierarchy()

TextFilters final class

public/minn/src/Minn/Content/TextFilters.php · 161 lines · Open the class →

The small text filters the reference runs over content, titles and comments, each from its observed rules (contracts/runtime.md "The content filters", "The comment form"): smilies, the capital P, insecure home addresses, the feed's embed clean-up, and a comment's links and spans.

staticsmiliesSearch(array $codes): string

The search pattern smilies_init leaves in $wp_smiliessearch, from the codes in their stored order (reverse-sorted): one alternative per first character, its code's rest quoted and joined, each bounded by whitespace, a no-break space or the line's ends (probe functions-utils).

@param list<string> $codes
staticsmilies(string $text, array $table, Closure $image): string

Smilies become their emoji, or an image for the few that have one: a code only counts as a whole word between spaces, and nothing inside a code, pre, script, style or textarea element, or inside a tag, changes.

@param array<string, string> $table code => emoji, or an image file name
@param Closure(string $file, string $code): string $image the <img> for an image smiley
staticcapitalP(string $text): string

"Wordpress" spelled right after a space, a parenthesis, a tag, or an opening curly quote; elsewhere it stays.

staticcapitalPEverywhere(string $text): string

"Wordpress" spelled right everywhere, as a title is.

staticsecureHome(string $content, string $insecure, string $secure): string

The site's own http address made https, escaped forms included.

staticfeedEmbeds(string $content): string

A feed carries an embedded post's iframe without the style that hides it until its script runs.

staticrel(string $slashed, string $rel, Closure $internal): string

Every link given a rel (wp_rel_nofollow, wp_rel_ugc), on slashed text as the content filters carry it, slashed again after (probes comment-fields, plugin-queue5). A link to the site itself does not take "nofollow".

@param Closure(string $href): bool $internal whether an href points at the site itself
staticlinkRel(string $inner, string $rel, Closure $internal): string

One link's opening tag with the rel added (wp_rel_callback): a tag that has a rel keeps its words first, gains the new ones, and is written again with every attribute double-quoted and rel last; one without keeps its attributes as written and takes rel at the end.

@param Closure(string $href): bool $internal
staticnoteMentionClasses(string $slashed): string

A span keeps no class in a comment (where a note's mention would be faked); slashed text in, slashed out.

insideignoring(), words(), attributes()

Texturize final class

public/minn/src/Minn/Content/Texturize.php · 269 lines · Open the class →

The texturize subset the reference applies to rendered text: straight quotes, apostrophes, ellipses, dashes, and primes become numeric entities, outside the skip elements only. Coverage is pinned by the texturize battery post; anything beyond it is a documented gap.

staticenabled(): bool

Whether text is texturized at all (run_wptexturize): asked on the first text of a request and again after askAgain(); once the answer is no, it holds for the rest of the request (probe filter-inputs-3).

staticaskAgain(): void

Has the next text ask run_wptexturize again (wptexturize's reset), unless the answer was already no.

staticcharacters(): array

The characters texturizing writes, as the site's language writes them (each asked by its translation context, as the reference asks them): the dashes, the curly quotes, the apostrophe and the primes.

@return array{en: string, em: string, open1: string, close1: string, apostrophe: string, open2: string, close2: string, prime: string, prime2: string}
staticskipPattern(): string

The elements whose text stays as written (no_texturize_tags: pre, code, kbd, style, script and tt), read on every text, as a pattern alternation.

statichtml(string $html, ?string $skip = NULL): string

Curly quotes, dashes, and ellipses in the text of HTML, leaving tags and the skipped elements alone; registered shortcodes stay as written (probe shortcode-run), and so does the text inside the ones no_texturize_shortcodes names ([code] by default). Nothing changes once texturizing is off. A caller that already read the skip pattern hands it over, so each text asks the filters once.

staticentities(string $html, string $skip): string

Bare ampersands made entities outside the skipped elements, as wptexturize leaves them after the rest.

statictext(string $text): string

The same substitutions on a plain string with no tags.

staticsplitPattern(string $shortcodes = ''): string

The pattern that cuts markup for texturizing (wp_html_split's pieces with comments whole and, given, shortcodes): comments run to "-->" or the end, a tag to its first ">", and each piece is captured.

staticshortcodeTags(array $tags): string

The pattern fragment for shortcodes with these names: an opening or closing tag, or an escaped [[one]], its name ended by a space, "/" or "]", its attributes up to the first "]".

@param list<string> $tags
staticpushPop(string $text, array $stack, array $disabled): void

Keeps the stack of open elements texturize leaves alone: an opening tag or shortcode of a listed name is pushed (a self-closing one is not), its closing one pops it when it is the last opened.

@param list<string> $stack
@param list<string> $disabled
staticprimes(string $haystack, string $needle, string $prime, string $closeQuote): string

Each $needle in the text made a prime after a digit; otherwise a double quote becomes the closing quote, and a single one does only where a word ends (before the end, whitespace, a no-break space or . , ; : ! ? ) ] } -), staying an apostrophe or opening quote elsewhere.

insideshortcodePattern(), quiet(), endsWord()

UserRecord final readonly class

public/minn/src/Minn/Content/UserRecord.php · 96 lines · implements ArrayAccess · Open the class →

One row of the users table, read by name. Columns keep their WordPress spelling in row(); here they are $user->login, ->email, ->displayName. Array access is the migration bridge, read-only; new code reads the properties.

propreadonly int $id
propreadonly string $login
propreadonly string $passwordHash
propreadonly string $nicename
propreadonly string $email
propreadonly string $url
propreadonly string $registered
propreadonly string $activationKey
propreadonly int $status
propreadonly string $displayName
staticfromRow(array $row): self

A record from a users row; a missing column reads as empty.

@param array<string, mixed> $row a users-table row, joined columns welcome
staticfromRows(array $rows): array

A record for every row, in order.

@param list<array<string, mixed>> $rows @return list<self>
methodrow(): array

The stored row, for the writers and shapers that still spell columns.

The row as the table holds it.

methodname(): string

The name shown for this person: the display name, or the login when none was set.

methodcolumn(string $name): mixed

One stored column by its database name, or null.

methodoffsetExists(mixed $offset): bool

The migration bridge: the record answers to its column names the way the row did.

methodoffsetGet(mixed $offset): mixed

The migration bridge: one column by its stored name, or null.

methodoffsetSet(mixed $offset, mixed $value): never

Records are read-only; writes go through the repository.

methodoffsetUnset(mixed $offset): never

Records are read-only; writes go through the repository.

Users final readonly class

public/minn/src/Minn/Content/Users.php · 286 lines · Open the class →

new__construct(Minn\Db $db)
methodfind(int $id): ?Minn\Content\UserRecord

The user with this id, or null.

methodfindByLogin(string $login): ?Minn\Content\UserRecord

The user with this login, or null.

methodfindByEmail(string $email): ?Minn\Content\UserRecord

The user with this email, or null.

methodfindBySlug(string $nicename): ?Minn\Content\UserRecord

The user with this nicename, or null.

methoduniqueNicename(string $base, int $skipId = 0): string

The unique-nicename rule: sanitized login, -2, -3 on collision.

methodcreateAccount(array $fields): int

Inserts a user and the 14 default meta rows the reference writes.

login: string, email: string, password: string, role: string, display_name: string, url?: string, nicename?: string, nickname?: string, first_name?: string, last_name?: string, description?: string, locale?: string, } $fields

@param array{
staticdefaultDisplayName(string $first, string $last, string $login): string

The display name an account gets when none is given, as the reference picks it: first and last name, whichever of the two there is, or the login. The nickname plays no part.

methodinsertAccount(array $fields): int

A new account's row alone: the caller adds its meta and role, and keeps the count. @param array<string, mixed> $fields

@param array<string, mixed> $fields
staticprofileMeta(array $fields): array

The twelve profile meta rows the reference gives a new account, in its order (the role's two follow).

@param array<string, mixed> $fields
@return array<string, string>
methodrecount(): void

Recounts the accounts into user_count, quietly.

methoddeleteRow(int $id): void

Removes the account's row alone: the caller removes its meta and keeps the count.

methodinsert(array $columns): int

Inserts a users row from column => value pairs and returns the new id.

@param array<string, mixed> $columns
methodupdate(int $id, array $columns): void

Sets the given columns on one user.

@param array<string, mixed> $columns
methoddelete(int $id): void

Removes the user and their meta; their posts are reassigned or removed first by the caller.

methodids(int $limit): array

User ids in login order, optionally the first N. @return list<int>

@return list<int>
methodsetPassword(int $userId, string $hash): void

Stores a new password hash and clears any pending reset key.

methodcount(): int

How many users the site has.

methodregisteredAfter(string $since, int $limit): array

The newest users registered since a site-local time, up to the limit.

@return list<array> the newest registrations after a site-local timestamp
methoddeleteMeta(int $userId, string $key): void

Removes one meta key from a user.

methodwithCapabilities(string $capabilitiesKey): array

Every user with a capabilities row under the key given, as the old get_users_of_blog listed them: id twice, login, display name, email and the stored capabilities, in id order.

@return list<array<string, string>>
methodwithoutRole(string $capabilitiesKey, string $rolePattern): array

The ids of users whose capabilities row names none of the roles (a pattern of their names). @return list<string>

@return list<string>
methodmeta(int $userId, string $key): ?string

One usermeta value, raw. Serialized blobs come back as stored.

methodsetMeta(int $userId, string $key, string $value): void

Replaces one usermeta row. The (user_id, meta_key) pair carries no unique index on the stock schema, so this is a delete plus insert rather than an upsert.

insiderecord(), refreshCount(), writeAccount()