Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
CHANGELOG
=========

### 1.7.0 (unreleased) ###

* Add `SpaceBeforePunctuation` fixer for locale-aware punctuation spacing
* Add `LocaleConfig` class for centralized locale configuration
* Extend `SmartQuotes` to support 45+ languages via `LocaleConfig`
* Deprecate `FrenchNoBreakSpace` fixer in favor of `SpaceBeforePunctuation`

### 1.6.0 (2025-12-15) ###

* Reduced the package size
Expand Down
49 changes: 45 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Just tell the Fixer class [which Fixer](#available-fixers) you want to run on yo
```php
use JoliTypo\Fixer;

$fixer = new Fixer(['SmartQuotes', 'FrenchNoBreakSpace']);
$fixer = new Fixer(['SmartQuotes', 'SpaceBeforePunctuation']);
$fixer->setLocale('fr_FR');

$fixedContent = $fixer->fix('<p>Je suis "très content" de t\'avoir invité sur <a href="http://jolicode.com/">Jolicode.com</a> !</p>');
Expand Down Expand Up @@ -136,8 +136,22 @@ and do not forget to specify a locale on the Fixer instance.

This Fixer replaces legacy `EnglishQuotes`, `FrenchQuotes` and `GermanQuotes`.

FrenchNoBreakSpace
------------------
SpaceBeforePunctuation
----------------------

Locale-aware fixer for spacing before punctuation marks. Handles:
- **French** (`fr`, `fr_FR`, `fr_BE`, `fr_CH`): Adds non-breaking space before `:` and thin non-breaking space before `;`, `!`, `?`
- **Canadian French** (`fr_CA`): No space before punctuation (follows English conventions)
- **Swiss German** (`de_CH`): Uses French-style guillemets with thin spaces
- **All other locales**: Removes any incorrect space before punctuation

This fixer requires a locale to be set on the Fixer with `$fixer->setLocale('fr_FR');`.

FrenchNoBreakSpace (deprecated)
-------------------------------

> [!WARNING]
> This fixer is deprecated. Use `SpaceBeforePunctuation` instead.

Replaces some classic spaces by non-breaking spaces following the French typographic code.
No break space are placed before `:`, thin no break space before `;`, `!` and `?`.
Expand Down Expand Up @@ -200,7 +214,7 @@ fr_FR
Those rules apply for most of the recommendations of "Abrégé du code typographique à l'usage de la presse", ISBN: 9782351130667.

```php
$fixer = new Fixer(['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'FrenchNoBreakSpace', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark']);
$fixer = new Fixer(['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'SpaceBeforePunctuation', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark']);
$fixer->setLocale('fr_FR');
```

Expand All @@ -226,6 +240,26 @@ $fixer->setLocale('de_DE');

More to come (contributions welcome!).

Locale support for spacing and quotes
-------------------------------------

JoliTypo supports locale-specific rules for spacing before punctuation and quotation marks:

| Locale | Space Before `: ; ! ?` | Quote Style |
|--------|------------------------|-------------|
| fr_FR, fr_BE, fr_CH | YES (nbsp/nnbsp) | « text » |
| fr_CA | NO | « text » |
| de_DE, de_AT | NO | „text“ |
| de_CH | NO | «text» |
| en_* | NO | “text” |
| es_*, it_*, pt_* | NO | «text» |
| pl_*, cs_*, sk_*, hu_*, ro_*, bg_* | NO | „text“ |
| ru_*, uk_*, be_* | NO | «text» |
| sv_*, fi_* | NO | "text" |
| nl_*, tr_* | NO | "text" |

See `LocaleConfig::QUOTE_STYLES_BY_LOCALE` for the complete list of supported languages.

Documentation
=============

Expand Down Expand Up @@ -351,6 +385,13 @@ Thanks to theses online resources for helping a developer understand typography:
- [FR] "Abrégé du code typographique à l'usage de la presse", ISBN: 9782351130667
- https://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks

Typography rules by language:

- https://type.today/en/journal/spaces - Comprehensive guide on spacing in typography
- https://type.today/en/journal/quotes - Comprehensive guide on quotation marks by language
- https://www.mancko.com/typography-punctuation/en/ - Multi-language typography reference
- [FR] https://fr.wikipedia.org/wiki/Ponctuation#Espaces_et_ponctuation - French punctuation spacing rules

<br><br>
<div align="center">
<a href="https://jolicode.com/"><img src="https://jolicode.com/media/original/oss/footer-github.png?v3" alt="JoliCode is sponsoring this project"></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ private function createPresetDefinition(ContainerBuilder $container, array $conf
}

$definition->addArgument($fixers);
$container->setDefinition(sprintf('joli_typo.fixer.%s', $name), $definition);
$container->setDefinition(\sprintf('joli_typo.fixer.%s', $name), $definition);

$presets[$name] = new Reference(sprintf('joli_typo.fixer.%s', $name));
$presets[$name] = new Reference(\sprintf('joli_typo.fixer.%s', $name));
}

return $presets;
Expand Down
2 changes: 1 addition & 1 deletion src/JoliTypo/Bridge/Twig/JoliTypoExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function getFilters(): array
public function translate($text, $preset = 'default'): string
{
if (!isset($this->presets[$preset])) {
throw new InvalidConfigurationException(sprintf('There is no "%s" preset configured.', $preset));
throw new InvalidConfigurationException(\sprintf('There is no "%s" preset configured.', $preset));
}

return $this->presets[$preset]->fix($text);
Expand Down
2 changes: 1 addition & 1 deletion src/JoliTypo/Exception/InvalidMarkupException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@

class InvalidMarkupException extends \RuntimeException
{
protected $message = 'An error happened when trying to read your HTML with \\DOMDocument.';
protected $message = 'An error happened when trying to read your HTML with \DOMDocument.';
}
21 changes: 10 additions & 11 deletions src/JoliTypo/Fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ class Fixer
public const COPY = '©'; // &copy;
public const ALL_SPACES = "\xE2\x80\xAF|\xC2\xAD|\xC2\xA0|\\s"; // All supported spaces, used in regexps. Better than \s

public const RECOMMENDED_RULES_BY_LOCALE = [
'en_GB' => ['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark'],
'fr_FR' => ['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'FrenchNoBreakSpace', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark'],
'fr_CA' => ['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark'],
'de_DE' => ['Ellipsis', 'Dimension', 'Unit', 'Dash', 'SmartQuotes', 'NoSpaceBeforeComma', 'CurlyQuote', 'Hyphen', 'Trademark'],
];
/**
* @deprecated since 1.7.0, use LocaleConfig::RECOMMENDED_RULES_BY_LOCALE instead
* @see LocaleConfig::RECOMMENDED_RULES_BY_LOCALE
*/
public const RECOMMENDED_RULES_BY_LOCALE = LocaleConfig::RECOMMENDED_RULES_BY_LOCALE;

private array $protectedTags = ['head', 'link', 'pre', 'code', 'script', 'style'];

Expand Down Expand Up @@ -193,17 +192,17 @@ private function compileRules(array $rules): void
$className = $rule::class;
} else {
$className = class_exists($rule) ? $rule : (class_exists(
'JoliTypo\\Fixer\\' . $rule
) ? 'JoliTypo\\Fixer\\' . $rule : false);
'JoliTypo\Fixer\\' . $rule
) ? 'JoliTypo\Fixer\\' . $rule : false);
if (!$className) {
throw new BadRuleSetException(sprintf('Fixer %s not found', $rule));
throw new BadRuleSetException(\sprintf('Fixer %s not found', $rule));
}

$fixer = new $className($this->getLocale());
}

if (!$fixer instanceof FixerInterface) {
throw new BadRuleSetException(sprintf('%s must implement FixerInterface', $className));
throw new BadRuleSetException(\sprintf('%s must implement FixerInterface', $className));
}

$this->_rules[$className] = $fixer;
Expand Down Expand Up @@ -351,7 +350,7 @@ private function exportDOMDocument(\DOMDocument $dom): string
// Remove added body & doctype
$content = preg_replace(
[
'/^\\<\\!DOCTYPE.*?<html>.*?<body>/si',
'/^\<\!DOCTYPE.*?<html>.*?<body>/si',
'!</body>\n?</html>$!si',
],
'',
Expand Down
17 changes: 10 additions & 7 deletions src/JoliTypo/Fixer/FrenchNoBreakSpace.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

namespace JoliTypo\Fixer;

use JoliTypo\Fixer;
use JoliTypo\FixerInterface;
use JoliTypo\StateBag;

Expand All @@ -19,16 +18,20 @@
* NO_BREAK_SPACE inside « ».
*
* As recommended by "Abrégé du code typographique à l'usage de la presse", ISBN: 978-2351130667
*
* @deprecated since 1.7.0, use SpaceBeforePunctuation instead
*/
class FrenchNoBreakSpace implements FixerInterface
{
public function fix(string $content, ?StateBag $stateBag = null)
{
$content = preg_replace('@[' . Fixer::ALL_SPACES . ']+(:)@mu', Fixer::NO_BREAK_SPACE . '$1', $content);
$content = preg_replace('@[' . Fixer::ALL_SPACES . ']+([;!\?])@mu', Fixer::NO_BREAK_THIN_SPACE . '$1', $content);
private SpaceBeforePunctuation $delegate;

$content = preg_replace('@' . Fixer::LAQUO . '[' . Fixer::ALL_SPACES . ']?@mu', Fixer::LAQUO . Fixer::NO_BREAK_SPACE, $content);
public function __construct()
{
$this->delegate = new SpaceBeforePunctuation('fr_FR');
}

return preg_replace('@[' . Fixer::ALL_SPACES . ']?' . Fixer::RAQUO . '@mu', Fixer::NO_BREAK_SPACE . Fixer::RAQUO, $content);
public function fix(string $content, ?StateBag $stateBag = null): string
{
return $this->delegate->fix($content, $stateBag);
}
}
Loading
Loading