Extract inline directives (`:name[label]{attrs}`) from running prose. Recognition rules keep prose safe: - the `:` must sit at a word boundary (start of text, or after a non-alphanumeric character) — so `did:atomic:lee` never matches; - the name must be immediately followed by `[label]` — a bare `:word` is prose; - the name must be in the inline registry — `:unknown[x]` stays prose, because prose
(text: &str)
| 140 | /// - the name must be in the inline registry — `:unknown[x]` stays prose, |
| 141 | /// because prose is the unconstrained slot. |
| 142 | pub fn parse_inline(text: &str) -> Result<Vec<Directive>> { |
| 143 | let bytes = text.as_bytes(); |
| 144 | let mut out = Vec::new(); |
| 145 | let mut i = 0; |
| 146 | while i < bytes.len() { |
| 147 | if bytes[i] != b':' { |
| 148 | i += 1; |
| 149 | continue; |
| 150 | } |
| 151 | // Word boundary: previous byte must not be alphanumeric or ':' (which |
| 152 | // would make this the tail of a URN/DID or a block-directive marker). |
| 153 | if i > 0 { |
| 154 | let prev = bytes[i - 1] as char; |
| 155 | if prev.is_ascii_alphanumeric() || prev == ':' { |
| 156 | i += 1; |
| 157 | continue; |
| 158 | } |
| 159 | } |
| 160 | // Read the name: [a-z0-9-]+ immediately after ':'. |
| 161 | let name_start = i + 1; |
| 162 | let mut j = name_start; |
| 163 | while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'-') { |
| 164 | j += 1; |
| 165 | } |
| 166 | if j == name_start || j >= bytes.len() || bytes[j] != b'[' { |
| 167 | i += 1; |
| 168 | continue; |
| 169 | } |
| 170 | let name = &text[name_start..j]; |
| 171 | if !vocab::is_known_inline_directive(name) { |
| 172 | i += 1; |
| 173 | continue; |
| 174 | } |
| 175 | // Label: up to the matching ']' (no nesting in labels). |
| 176 | let label_start = j + 1; |
| 177 | let Some(label_len) = text[label_start..].find(']') else { |
| 178 | return Err(CanonicalError::Directive(format!( |
| 179 | "unterminated inline directive ':{name}[' (missing closing ']')" |
| 180 | ))); |
| 181 | }; |
| 182 | let label = text[label_start..label_start + label_len].to_string(); |
| 183 | let mut end = label_start + label_len + 1; |
| 184 | // Optional immediate `{attrs}`. |
| 185 | let mut attrs_src = ""; |
| 186 | if bytes.get(end) == Some(&b'{') { |
| 187 | let Some(close) = text[end + 1..].find('}') else { |
| 188 | return Err(CanonicalError::Directive(format!( |
| 189 | "unterminated inline directive ':{name}[…]{{' (missing closing '}}')" |
| 190 | ))); |
| 191 | }; |
| 192 | attrs_src = &text[end + 1..end + 1 + close]; |
| 193 | end = end + 1 + close + 1; |
| 194 | } |
| 195 | let (id, attrs) = parse_attrs(attrs_src)?; |
| 196 | out.push(Directive { |
| 197 | name: name.to_string(), |
| 198 | id, |
| 199 | attrs, |