. "Safely" * carries a lot of weight — a naive str_replace over 300 hand-written posts * would corrupt shortcode attributes, nest anchors, rewrite alt text and break * Gutenberg block delimiters, and the user would not discover it for weeks. * * Two lessons are inherited from the in-body image work in the main plugin: * * 1. Never assume

exists. Classic-editor content stores paragraphs as * bare text between block elements and lets wpautop() create the

at * render time — a real post can contain zero

in post_content. This is * the same trap documented at aicobr_prose_anchor_offset(); a linker that * anchors on paragraphs silently no-ops on exactly the older hand-written * posts this feature exists to serve. * * 2. Apply edits from the HIGHEST offset backwards, so earlier offsets stay * valid as the string grows. Same discipline as aicobr_insert_figure(). * * Offsets are BYTE offsets, consistent with preg_* PREG_OFFSET_CAPTURE and with * aicobr_find_headings() in the main plugin. Multibyte safety comes from every * pattern carrying the /u flag, so a match can only ever begin and end on a * character boundary — never from mb_* string juggling, which would disagree * with the rest of the codebase. */ if (!class_exists('AICOBR_Link_Scanner')) { class AICOBR_Link_Scanner { /** * Elements whose ENTIRE contents are off limits, not merely their tags. * * a — anchors cannot nest, and re-linking existing link text is * both invalid HTML and an over-optimisation signal. * h1..h6 — linking a heading reads as spam and breaks the visual * hierarchy the formatter builds. * figure — covers

, including ACB's own acb-fig captions. * code/pre/ — a keyword inside a code sample is not prose. * kbd/samp * script/ — self-evident; also protects inline JSON. * style * blockquote — inserting a link into quoted material silently alters * somebody else's words. Editorially wrong rather than * technically unsafe, so it is filterable, but the default * is to leave quotations alone. * textarea/ — these hold TEXT that is not prose. An inside