diff --git a/src/Performance/BorrowedHtmlLayout.php b/src/Performance/BorrowedHtmlLayout.php
index 8995073..1fb3d56 100644
--- a/src/Performance/BorrowedHtmlLayout.php
+++ b/src/Performance/BorrowedHtmlLayout.php
@@ -423,9 +423,22 @@ private function renderInline(string $text, array $definitions): ?string
if (preg_match('/^\[[^\]]+\]: +\S+ +"[^"]*"$/', $text) === 1) {
return $this->escapeText($text);
}
+ if (
+ strpbrk($text, "*_`[{}^\\<>~!@$=#'\":") === false
+ && !str_contains($text, '--')
+ && !str_contains($text, '...')
+ && !str_contains($text, '(c)')
+ && !str_contains($text, '(r)')
+ && !str_contains($text, '(tm)')
+ ) {
+ return $this->escape($text);
+ }
if ($this->inlineComplex($text)) {
return null;
}
+ if (strpbrk($text, '*_`[') === false) {
+ return $this->escapeText($text);
+ }
$flat = $this->renderFlatInline($text, $definitions);
if ($flat !== false) {
return $flat;
@@ -745,12 +758,15 @@ private function decimalListItem(string $line): ?array
private function inlineComplex(string $text): bool
{
- $withoutContractions = preg_replace('/(?<=[A-Za-z0-9])\'(?=[A-Za-z0-9])/', '', $text);
- if ($withoutContractions === null) {
- return true;
+ $withoutContractions = $text;
+ if (str_contains($text, "'")) {
+ $withoutContractions = preg_replace('/(?<=[A-Za-z0-9])\'(?=[A-Za-z0-9])/', '', $text);
+ if ($withoutContractions === null) {
+ return true;
+ }
}
- if (substr_count($withoutContractions, '"') % 2 !== 0) {
+ if (str_contains($withoutContractions, '"') && substr_count($withoutContractions, '"') % 2 !== 0) {
return true;
}
@@ -857,22 +873,28 @@ private function escape(string $text): string
private function escapeText(string $text): string
{
- $text = (string)preg_replace('/(?<=[A-Za-z0-9])\'(?=[A-Za-z0-9])/', '’', $text);
- $text = (string)preg_replace('/"([^"]*)"/', '“$1”', $text);
- $text = (string)preg_replace_callback('/-{2,}/', static function (array $match): string {
- $length = strlen($match[0]);
- if ($length % 2 === 0 && $length % 3 !== 0) {
- return str_repeat('–', intdiv($length, 2));
- }
- $triples = intdiv($length, 3);
- $remainder = $length % 3;
- if ($remainder === 1) {
- $triples--;
- $remainder = 4;
- }
-
- return str_repeat('—', $triples) . str_repeat('–', intdiv($remainder, 2));
- }, $text);
+ if (str_contains($text, "'")) {
+ $text = (string)preg_replace('/(?<=[A-Za-z0-9])\'(?=[A-Za-z0-9])/', '’', $text);
+ }
+ if (str_contains($text, '"')) {
+ $text = (string)preg_replace('/"([^"]*)"/', '“$1”', $text);
+ }
+ if (str_contains($text, '--')) {
+ $text = (string)preg_replace_callback('/-{2,}/', static function (array $match): string {
+ $length = strlen($match[0]);
+ if ($length % 2 === 0 && $length % 3 !== 0) {
+ return str_repeat('–', intdiv($length, 2));
+ }
+ $triples = intdiv($length, 3);
+ $remainder = $length % 3;
+ if ($remainder === 1) {
+ $triples--;
+ $remainder = 4;
+ }
+
+ return str_repeat('—', $triples) . str_repeat('–', intdiv($remainder, 2));
+ }, $text);
+ }
return $this->escape($text);
}
diff --git a/tests/BorrowedHtmlLayoutTest.php b/tests/BorrowedHtmlLayoutTest.php
index 1cb33a8..26a1f5c 100644
--- a/tests/BorrowedHtmlLayoutTest.php
+++ b/tests/BorrowedHtmlLayoutTest.php
@@ -28,6 +28,7 @@ public function testAcceptedDocumentsAreByteIdenticalToTheAstPipeline(string $so
public static function acceptedDocuments(): iterable
{
yield 'plain paragraphs' => ["First paragraph.\ncontinues here.\n\nSecond paragraph.\n"];
+ yield 'smart punctuation' => ["A \"quote\", don't stop--now.\n"];
yield 'core inline' => ["A *strong*, _emphasized_, and `coded` [link](https://example.com).\n"];
yield 'sections' => ["# First heading\n\nBody.\n\n## Child heading\n\nMore body.\n"];
yield 'code fence' => ["# Code\n\n```php\necho '';\n```\n"];