# ucharclasses misbehaves with Spacing Modifier Letters and Combining Diacritical Marks

The ucharclasses package makes it possible to automatically switch fonts (or do other useful things) based on the Unicode block of characters. However, it seems to be having trouble resetting the font after spacing modifier letters and combining diacritical marks, when used with fontspec (in this case, with XeLaTeX).

I've found a few approaches towards solving this problem, but none of them seem to fix it entirely.

I've made minimum working examples based on the normal use and several proposals I've found for dealing with related issues. All of the examples attempt to set a serif font (DejaVu Serif) for IPA Extensions, Combining Diacritical Marks, and Spacing Modifier Letters, and use a sans-serif font (DejaVu Sans) for everything else. Both fonts contain glyphs for all the characters tested with.

1. This mwe uses the conventional approach to set the font for these

\documentclass{article}

\usepackage{polyglossia}
\setdefaultlanguage[variant=british]{english}
\usepackage[Latin, Phonetics, Diacritics, SpacingModifierLetters]{ucharclasses}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}

\setTransitionsFor{IPAExtensions}{\dejavuserif}{\normalfont}
\setTransitionsFor{CombiningDiacriticalMarks}{\dejavuserif}{\normalfont}
\setTransitionsFor{SpacingModifierLetters}{\dejavuserif}{\normalfont}

\begin{document}
\thispagestyle{empty}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] thaaw

\end{document}


In the output of this example, all characters in a "word" after a spacing modifier letter (in this case [ʰ], U+02B0) or combining diacritical mark (in this case [◌́], U+301) are displayed in the default sans-serif font.

2. This mwe uses a slight modification of the conventional approach, based on this answer.

\documentclass{article}

\usepackage{polyglossia}
\setdefaultlanguage[variant=british]{english}
\usepackage[Latin, Phonetics, Diacritics, SpacingModifierLetters]{ucharclasses}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}

\setTransitionTo{IPAExtensions}{\dejavuserif}
\setTransitionFrom{IPAExtentions}{\normalfont}
\setTransitionTo{CombiningDiacriticalMarks}{\dejavuserif}
\setTransitionFrom{CombiningDiacriticalMarks}{\normalfont}
\setTransitionTo{SpacingModifierLetters}{\dejavuserif}
\setTransitionFrom{SpacingModifierLetters}{\normalfont}

\begin{document}
\thispagestyle{empty}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] thaaw

\end{document}


I didn't expect this approach to be any different from the conventional one, but there is a small difference: after the one "word" without one of the problem characters (i.e., [tɑɑɯ]), all the characters ("] [t") until the next problem character (ʰ) are rendered using the serif font. Otherwise, it still renders the remaining characters after the problematic characters with the sans-serif font.

3. This mwe uses an approach presented in this answer.

\documentclass{article}

\usepackage{polyglossia}
\setdefaultlanguage[variant=british]{english}

\usepackage[Latin, Phonetics, Diacritics, SpacingModifierLetters]{ucharclasses}

\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}

\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}

\setTransitionsFor{IPAExtensions}{\begingroup\dejavuserif}{\endgroup}
\setTransitionsFor{CombiningDiacriticalMarks}{\begingroup\dejavuserif}{\endgroup}
\setTransitionsFor{SpacingModifierLetters}{\begingroup\dejavuserif}{\endgroup}

\begin{document}
\thispagestyle{empty}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] thaaw

\end{document}


The output of this approach is slightly better than the previous ones, in that the Combining Diacritical Mark (◌́) behaves as expected—i.e., the characters after it are all rendered in the serif font, but only if there's not another problematic character before it (like ʰ). It also produces the following errors to the console:

! Extra \endgroup.
<XeTeXinterchartoks> \endgroup

l.23 tháaw [tʰɑɑɯ]
[tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] tháaw
?
! Extra \endgroup.
<XeTeXinterchartoks> \endgroup

l.23 thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ]
[tɑ́ɑɯ] thaaw
?
[1] (./2016-04-02b.aux) )

4. This mwe uses the solution suggested at this answer.

\documentclass{article}

\usepackage{polyglossia}
\setdefaultlanguage[variant=british]{english}
\usepackage[Latin, Phonetics, Diacritics, SpacingModifierLetters]{ucharclasses}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}

\makeatletter
\setTransitionsFor{IPAExtensions}
{\let\curfamily\f@family\let\curshape\f@shape\let\curseries\f@series\dejavuserif}
{\fontfamily{\curfamily}\fontshape{\curshape}\fontseries{\curseries}\selectfont}
\setTransitionsFor{SpacingModifierLetters}
{\let\curfamily\f@family\let\curshape\f@shape\let\curseries\f@series\dejavuserif}
{\fontfamily{\curfamily}\fontshape{\curshape}\fontseries{\curseries}\selectfont}
\setTransitionsFor{CombiningDiacriticalMarks}
{\let\curfamily\f@family\let\curshape\f@shape\let\curseries\f@series\dejavuserif}
{\fontfamily{\curfamily}\fontshape{\curshape}\fontseries{\curseries}\selectfont}
\makeatother

\begin{document}
\thispagestyle{empty}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] thaaw

\end{document}


The output of this approach seems to suggest that that the Combining Diacritical Mark (◌́) is not a problem—i.e., following characters in a word are displayed with the serif font if there isn't another character (like [ʰ]) making them sans serif—but the serif "spreads" into the next word as well.

I suspect this is a bug in or limitation of the ucharclasses package. Confirmation of this, or a solution or work-around would be quite welcome.

• One would have to make tests with the primitive \XeTeXcharclass etc to find out if it is a xetex or a ucharclass problem. But your character classes are not really disjoint. In your own example you are using "t" inside and outside the brackets. And what should happen with a combining accent used in normal text? A clear semantic markup is imho better. – Ulrike Fischer Apr 3 '16 at 11:50
• What do you mean that the classes are not really disjoint, @UlrikeFischer? I understand that I'm mixing blocks in words, but this is completely normal use of these characters, so I guess the behaviour should at least be predictable. Also, by "clear semantic markup", do you mean just defining a command for use with all phonetic transcriptions that sets the font (and anything else I might deem useful)? – Jonathan W. Apr 3 '16 at 15:44
• Also, my first response to your question would be that a combining diacritical mark in Latin text should probably be rendered in the serif font (given my definitions), but it makes me wonder whether combining marks and the character they're combining with can be different fonts (seems like a bad idea, even if possible?). – Jonathan W. Apr 3 '16 at 15:45

Mixing unicode blocks in words = humans writing; setting a font when entering a different unicode block (or leaving it) = ucharclasses.

So English and Vietnamese aren't distinguishable by which block a character belongs to, since they both share the Latin block. But English and Old Persian are distinguishable by character class.

The combining diacritical marks block is a different block to the Basic Latin one, so, yes, this is possible:

and even this:

MWE

\documentclass[12pt]{article}
\usepackage[no-math]{fontspec}
\usepackage[BasicLatin, CombiningDiacriticalMarks]{ucharclasses}
\usepackage{xcolor}

\setmainfont{Noto Serif}
\newfontfamily\fdiac[Colour=red,Scale=1.5]{Fira Sans Black}

\setTransitionTo{BasicLatin}{\normalfont}
\setTransitionTo{CombiningDiacriticalMarks}{\fdiac}

\begin{document}
\large
a a\symbol{"0302} xyẑ abc \ \ o\symbol{"0302}\symbol{"0344}o\symbol{"0302}\symbol{"0321}\symbol{"0325}\symbol{"032C}

\end{document


"Disjoint" means that ucharclasses can produce only one output (at a time), not two or more, so that in turn means that the sets of characters to process should not overlap or share elements.

These combining marks could be really useful.

The sign for the "Hm, oh, er, um, that's a really nice..." conversation filler, as used in polite baboon social interactions among deferential individuals, say.

\documentclass[12pt]{article}
\usepackage[no-math]{fontspec}
\usepackage[BasicLatin, CombiningDiacriticalMarks]{ucharclasses}
\usepackage{xcolor}

\setmainfont{Noto Serif}
\newfontfamily\fdiac[Colour=red,Scale=1.5]{Fira Sans Black}
\newfontfamily\fdiacb[Colour=blue,Scale=2.5]{Gentium Plus}

\setTransitionTo{BasicLatin}{\normalfont}
\setTransitionTo{CombiningDiacriticalMarks}{\fdiac}

\begin{document}
\large
(o\symbol{"0302}\symbol{"032B}{\let\fdiac\fdiacb\symbol{"0308}\symbol{"036A}}o\symbol{"0302}\symbol{"0321}\symbol{"0325}\symbol{"032C})

\end{document}


Further edit

On the presumption that transitioning requires a (sequential?) transition, insert a transition, using either {} or a zero-width joiner (both being outside the relevant code blocks):

Diacritical mark and base character function as a unit (in a sense), font-wise, so insert a transition after the base character.

MWE

\documentclass{article}
\usepackage{xcolor}
\usepackage[Latin, Phonetics, Diacritics, SpacingModifierLetters]{ucharclasses}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}[Colour=red]

\setTransitionsFor{IPAExtensions}{\dejavuserif}{\normalfont}
\setTransitionsFor{CombiningDiacriticalMarks}{\dejavuserif}{\normalfont}
\setTransitionsFor{SpacingModifierLetters}{\dejavuserif}{\normalfont}

\newcommand\zwnj{^^^^200c}

\begin{document}

thaaw [tʰ{}ɑɑɯ] [tɑɑɯ] [tʰ{}ɑ́{}ɑɯ] [tɑ́{}ɑɯ] thaaw
\normalfont

thaaw [t^^^^02b0\zwnj ɑɑɯ] [tɑɑɯ] [tʰ\zwnj ɑ́\zwnj ɑɯ] [tɑ́\zwnj ɑɯ] thaaw

\end{document}


Although, keeping the units of meaning and display synchronized would be less of a cognitive load on the reader:

MWE

\documentclass{article}
\usepackage{xcolor}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}[Colour=red]
\newcommand\ph[1]{[{\dejavuserif #1}]}

\begin{document}

thaaw \ph{tʰɑɑɯ} \ph{tɑɑɯ} \ph{tʰɑ́ɑɯ} \ph{tɑ́ɑɯ} thaaw

\end{document}


On the matter of stacking diacritics, the font-designer's hand and choice comes into play.

Some random fonts, to illustrate:

Noto Serif

Acariya

Ajoure

Andika

Arial

DejaVu Serif

Looping

Hypothesis: The root cause is that counting starts from 1, and then goes upwards. Once only. So the last font-switch command put into the typesetting stream is the one that has a visible effect.

What happens when A block text and B block text are typed next to each other with no separator(s), the A-B transition code loops through all the blocks, finds A is ending, outputs the "coming out of A block" code, finds B is starting, outputs the "going into B block" code - if A codeblock is examined first.

If the A codeblock has a higher Unicode start/end point than the B code block, the looping finds instead that: B block is starting, outputs the "coming into B block" code, finds the A block is ending, outputs the "coming out of A block" code, and the user is surprised: we have gone back to normal font (for example).

In real life, the normal separator between blocks (intended as script blocks) is a space (Latin), which Tex converts to glue - but ZW characters from the punctuation block, as above, can also act as 'separators' between other blocks (technically, classes, not blocks).

Higher classes trump lower classes.

Ideally, explicitly specifying all the entry/exit pairwise combinations of code block transitions (where the code blocks are contiguous text) would cover the general case - except for cross-Unicode block text.

MWE

\documentclass{article}
\usepackage{xcolor}
\usepackage[Latin, Cyrillic, Cuneiform, Coptic]{ucharclasses}
\usepackage{fontspec}

\setmainfont{DejaVu Sans}
\newfontfamily\fa{Noto Sans Coptic}[Colour=red]
\newfontfamily\fb{Noto Serif}[Colour=blue]
\newfontfamily\fc{Noto Sans Cuneiform}[Colour=green]

\setTransitionsFor{Coptic}{\fa}{\normalfont}
\setTransitionsFor{Cyrillic}{\fb}{\normalfont}
\setTransitionsFor{Cuneiform}{\fc}{\normalfont}

\newcommand\zwnj{^^^^200c}

\begin{document}
ⲀⲁⲂⲃⲄⲅxАБВГДЕx𒀀𒀁𒀑𒀡𒀲xⲀⲁⲂⲃⲄⲅ

ⲀⲁⲂⲃⲄⲅАБВГДЕ𒀀𒀁𒀑𒀡𒀲ⲀⲁⲂⲃⲄⲅ

ⲀⲁⲂⲃⲄⲅ АБВГДЕ 𒀀𒀁𒀑𒀡𒀲 ⲀⲁⲂⲃⲄⲅ

xАБВГДЕxⲀⲁⲂⲃⲄⲅx𒀀𒀁𒀑𒀡𒀲xⲀⲁⲂⲃⲄⲅ

АБВГДЕⲀⲁⲂⲃⲄⲅ𒀀𒀁𒀑𒀡𒀲ⲀⲁⲂⲃⲄⲅ

АБВГДЕ ⲀⲁⲂⲃⲄⲅ 𒀀𒀁𒀑𒀡𒀲 ⲀⲁⲂⲃⲄⲅ

𒀀𒀁𒀑𒀡𒀲АБВГДЕⲀⲁⲂⲃⲄⲅⲀⲁⲂⲃⲄⲅ

𒀀𒀁𒀑𒀡𒀲 АБВГДЕ ⲀⲁⲂⲃⲄⲅ ⲀⲁⲂⲃⲄⲅ

\end{document}


Using \XeTeXinterchartoks transitions directly

Another way to have single-point transitions, instead of multiple, is to put (in this specific case) all three code blocks -- IPAExtensions, CombiningDiacriticalMarks, and SpacingModifierLetters -- into the same class; ucharclasses is not needed.

But that still leaves the semantic ambiguity that phonetic t and non-phonetic t are the same glyph.

MWE

(code adapted from an answer by Jonathon Kew on the TUG maillist 2008: here

\documentclass{article}
\usepackage{xcolor}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}[Colour=red]

\newcount\n
\n=\ɐ \loop \XeTeXcharclass \n=4 \ifnum\n<\ʯ \advance\n by 1 \repeat
%\n=\a \loop \XeTeXcharclass \n=4 \ifnum\n<\z \advance\n by 1 \repeat
\n=\ʰ \loop \XeTeXcharclass \n=4 \ifnum\n<\˿ \advance\n by 1 \repeat
\n=\̀ \loop \XeTeXcharclass \n=4 \ifnum\n<\ͯ \advance\n by 1 \repeat
% when we encounter class 4, we'll do \startling
\XeTeXinterchartoks 0 4 {\startling}
\XeTeXinterchartoks 4095 4 {\startling}
% and when we encounter class 0, we'll do \finishling
\XeTeXinterchartoks 4095 0 {\finishling}
\XeTeXinterchartoks 4 0 {\finishling}
%\newif\ifling
\newcommand\startling{\dejavuserif}
\newcommand\finishling{\normalfont}
\XeTeXinterchartokenstate=1

\begin{document}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] thaaw

\end{document}


Edit More on looping

Changing the sequence of the \setTransitionsFor commands affects the outcome:

etc

OK. ucharclasses wasn't designed for multiply-overlapping transitions: more of a 'into Greek, switch to Greek font; into Cyrillic, switch to a Cyrillic font; etc'.

The transitions are (leaving aside CJK matters, which take up classes 1,2,3):

(a) from/to class 0 (any glyph not defined in a class),

(b) from/to class 4095 (any non-glyph = glue, maths, boxes: collectively called 'boundary', as in word boundary; space becomes glue during typesetting, so that's why spaces are what I've been calling 'separators').

(c) any pair-wise transitions between user-defined classes (presumably 5,6,7,...)

So, reducing the complexity by having just three named ucharclasses, xxxClass, where xxx is the codeblock name (which makes things easier coding-wise, because we don't need to work out what the class numbers are), we have 12 'single' transitions: 3 into our classes from class 0, 3 into our classes from class 4095, and the 6 corresponding transitions out of our classes into classes 0 and 4095.

%singles ========================
%entering
%encountering our 3 classes
\XeTeXinterchartoks 0 \SpacingModifierLettersClass   = {\dejavuserif}
\XeTeXinterchartoks 0 \IPAExtensionsClass  = {\dejavuserif}
\XeTeXinterchartoks 0 \CombiningDiacriticalMarksClass  = {\dejavuserif}

% glue, maths, boxes etc = boundary'
\XeTeXinterchartoks 4095 \SpacingModifierLettersClass   = {\dejavuserif}
\XeTeXinterchartoks 4095 \IPAExtensionsClass   = {\dejavuserif}
\XeTeXinterchartoks 4095 \CombiningDiacriticalMarksClass   = {\dejavuserif}

%leaving
%encountering everything else
\XeTeXinterchartoks \SpacingModifierLettersClass 0  = {\normalfont}
\XeTeXinterchartoks \IPAExtensionsClass 0  = {\normalfont}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass 0  = {\normalfont}

% glue, maths, boxes etc = boundary'
\XeTeXinterchartoks \SpacingModifierLettersClass 4095  = {\normalfont}
\XeTeXinterchartoks \IPAExtensionsClass 4095  = {\normalfont}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass 4095  = {\normalfont}


Next, we have the pairwise-combinations of transitions into/out of our three classes, with respect to each other: 3x2=6 of them.

%pairs ===============
\XeTeXinterchartoks \SpacingModifierLettersClass \CombiningDiacriticalMarksClass  = {\dejavuserif}
\XeTeXinterchartoks \IPAExtensionsClass \CombiningDiacriticalMarksClass  = {\dejavuserif}

\XeTeXinterchartoks \SpacingModifierLettersClass \IPAExtensionsClass = {\dejavuserif}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass \IPAExtensionsClass = {\dejavuserif}

\XeTeXinterchartoks \IPAExtensionsClass \SpacingModifierLettersClass = {\dejavuserif}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass \SpacingModifierLettersClass = {\dejavuserif}


giving:

Full MWE:

\documentclass[varwidth,border=6pt]{standalone}
\usepackage{xcolor}
\usepackage[
%Latin,
%Phonetics,
%Diacritics,
SpacingModifierLetters,
CombiningDiacriticalMarks,
IPAExtensions,
]{ucharclasses}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}[Colour=red]

%\setTransitionsFor{CombiningDiacriticalMarks}{\dejavuserif}{\normalfont}
%\setTransitionsFor{SpacingModifierLetters}{\dejavuserif}{\normalfont}
%\setTransitionsFor{IPAExtensions}{\dejavuserif}{\normalfont}

%singles ========================
%entering
%encountering our 3 classes
\XeTeXinterchartoks 0 \SpacingModifierLettersClass   = {\dejavuserif}
\XeTeXinterchartoks 0 \IPAExtensionsClass  = {\dejavuserif}
\XeTeXinterchartoks 0 \CombiningDiacriticalMarksClass  = {\dejavuserif}

% glue, maths, boxes etc = boundary'
\XeTeXinterchartoks 4095 \SpacingModifierLettersClass   = {\dejavuserif}
\XeTeXinterchartoks 4095 \IPAExtensionsClass   = {\dejavuserif}
\XeTeXinterchartoks 4095 \CombiningDiacriticalMarksClass   = {\dejavuserif}

%leaving
%encountering everything else
\XeTeXinterchartoks \SpacingModifierLettersClass 0  = {\normalfont}
\XeTeXinterchartoks \IPAExtensionsClass 0  = {\normalfont}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass 0  = {\normalfont}

% glue, maths, boxes etc = boundary'
\XeTeXinterchartoks \SpacingModifierLettersClass 4095  = {\normalfont}
\XeTeXinterchartoks \IPAExtensionsClass 4095  = {\normalfont}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass 4095  = {\normalfont}

%pairs ===============
\XeTeXinterchartoks \SpacingModifierLettersClass \CombiningDiacriticalMarksClass  = {\dejavuserif}
\XeTeXinterchartoks \IPAExtensionsClass \CombiningDiacriticalMarksClass  = {\dejavuserif}

\XeTeXinterchartoks \SpacingModifierLettersClass \IPAExtensionsClass = {\dejavuserif}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass \IPAExtensionsClass = {\dejavuserif}

\XeTeXinterchartoks \IPAExtensionsClass \SpacingModifierLettersClass = {\dejavuserif}
\XeTeXinterchartoks \CombiningDiacriticalMarksClass \SpacingModifierLettersClass = {\dejavuserif}

\begin{document}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] [tʰɑ́ɑɯʰ] thaaw

\end{document}


Adding additional blocks/classes - like Latin or Phonetics - will increase the number of combinations and permutations to be covered (if space, or something else from class 4095 or what remains of class 0, is not going to be used to activate a transition event).

In that multi-coloured example using Coptic, Cyrillic, Cuneiform and Latin, the lines with the text strings next to each other with no spaces were corrected by specifying all the combinations:

Classes 4,5,6,7 were arbitarily used to (manually) class up the glyphs.

MWE

\documentclass[varwidth,border=6pt]{standalone}
\usepackage{xcolor}
\usepackage{fontspec}

\setmainfont{DejaVu Sans}
\newfontfamily\fa{Noto Sans Coptic}[Colour=red]
\newfontfamily\fb{Noto Serif}[Colour=blue]
\newfontfamily\fc{Noto Sans Cuneiform}[Colour=green]

\newcount\n
%===
%latin
\n=\A \loop \XeTeXcharclass \n=4 \ifnum\n<\Z \advance\n by 1 \repeat
\n=\a \loop \XeTeXcharclass \n=4 \ifnum\n<\z \advance\n by 1 \repeat
% when we encounter class 4, we'll do \startling
\XeTeXinterchartoks 0 4 {\startling}
\XeTeXinterchartoks 4095 4 {\startling}
% and when we encounter class 0, we'll do \finishling
\XeTeXinterchartoks 4095 0 {\finishling}
\XeTeXinterchartoks 4 0 {\finishling}
%\newif\ifling
\newcommand\startling{\normalfont}
\newcommand\finishling{}

%===
%cyrillic
\n=\Ѐ \loop \XeTeXcharclass \n=5 \ifnum\n<\ӿ \advance\n by 1 \repeat

% when we encounter class 5, we'll do \startling
\XeTeXinterchartoks 0 5 {\startlingcyr}
\XeTeXinterchartoks 4095 5 {\startlingcyr}
% and when we encounter class 0, we'll do \finishling
%\XeTeXinterchartoks 4095 0 {\finishlingcyr}
\XeTeXinterchartoks 5 0 {\finishlingcyr}
%\newif\ifling
\newcommand\startlingcyr{\fb}
\newcommand\finishlingcyr{\normalfont}
%===
%cuneiform
\n="12000 \loop \XeTeXcharclass \n=6 \ifnum\n<"12399 \advance\n by 1 \repeat
\XeTeXinterchartoks 0 6 {\startlingcun}
\XeTeXinterchartoks 4095 6 {\startlingcun}
\XeTeXinterchartoks 6 0 {\finishlingcun}
\newcommand\startlingcun{\fc}
\newcommand\finishlingcun{\normalfont}
%===
%coptic
\n=\Ⲁ \loop \XeTeXcharclass \n=7 \ifnum\n<\⳿ \advance\n by 1 \repeat
\XeTeXinterchartoks 0 7 {\startlingcop}
\XeTeXinterchartoks 1 7 {\startlingcop}
\XeTeXinterchartoks 2 7 {\startlingcop}
\XeTeXinterchartoks 3 7 {\startlingcop}
\XeTeXinterchartoks 5 7 {\startlingcop}
\XeTeXinterchartoks 6 7 {\startlingcop}
\XeTeXinterchartoks 4095 7 {\startlingcop}
\XeTeXinterchartoks 4095 0 {\finishlingcop}
\XeTeXinterchartoks 5 6 {\finishlingcyrc}
\XeTeXinterchartoks 7 0 {\finishlingcop}
\XeTeXinterchartoks 7 5 {\finishlingcopb}
\XeTeXinterchartoks 6 5 {\finishlingcopb}
\XeTeXinterchartoks 7 6 {\finishlingc}
\XeTeXinterchartoks 4 5 {\finishlingcopb}
\XeTeXinterchartoks 4 6 {\finishlingc}
\XeTeXinterchartoks 4 7 {\startlingcop}
\XeTeXinterchartoks 7 4 {\startling}
\XeTeXinterchartoks 6 4 {\startling}
\XeTeXinterchartoks 5 4 {\startling}

\newcommand\startlingcop{\fa}
\newcommand\finishlingcop{}
\newcommand\finishlingcyrc{\fc}
\newcommand\finishlingc{\fc}
\newcommand\finishlingcopb{\fb}

\XeTeXinterchartokenstate=1
\begin{document}
ⲀⲁⲂⲃⲄⲅxАБВГДЕx𒀀𒀁𒀑𒀡𒀲xⲀⲁⲂⲃⲄⲅ

ⲀⲁⲂⲃⲄⲅАБВГДЕ𒀀𒀁𒀑𒀡𒀲ⲀⲁⲂⲃⲄⲅ

ⲀⲁⲂⲃⲄⲅ АБВГДЕ 𒀀𒀁𒀑𒀡𒀲 ⲀⲁⲂⲃⲄⲅ

xАБВГДЕxⲀⲁⲂⲃⲄⲅx𒀀𒀁𒀑𒀡𒀲xⲀⲁⲂⲃⲄⲅ

АБВГДЕⲀⲁⲂⲃⲄⲅ𒀀𒀁𒀑𒀡𒀲ⲀⲁⲂⲃⲄⲅ

АБВГДЕ ⲀⲁⲂⲃⲄⲅ 𒀀𒀁𒀑𒀡𒀲 ⲀⲁⲂⲃⲄⲅ

𒀀𒀁𒀑𒀡𒀲АБВГДЕⲀⲁⲂⲃⲄⲅⲀⲁⲂⲃⲄⲅ

𒀀𒀁𒀑𒀡𒀲 АБВГДЕ ⲀⲁⲂⲃⲄⲅ ⲀⲁⲂⲃⲄⲅ

\end{document}

• This doesn't appear to address the issue of the font spreading—or if it does, I don't understand how. – Jonathan W. Jan 6 at 20:01
• @JonathanW. I presume entering and leaving codeblock(s) simultaneously is not possible. Therefore, insert a no-op transition, to act as a simultaneous To/From anchor, like a pedestrian-island in the typesetting sequence: {} is a good candidate at the latex level, or a zero-width glyph, at the font-level (although naming it would be prudent, in order to make it visible in the code). See edit. – Cicada Jan 11 at 12:30
• Okay, this is much better—I see two different proposed solutions in this answer. I'm not sure how the parts about stacking diacritics is relevant, though. – Jonathan W. Jan 13 at 1:31
• @JonathanW. I found out a bit more: the effects seen have to do with the overlapping - see further edits. Stacking diacritics is unrelated, in the sense that it was answering the implicit question "whether combining marks and the character they're combining with can be different fonts". – Cicada Jan 13 at 11:47
• actually, I had no doubt that combining diacritical marks and the character they're combining with could be different fonts. I didn't realise my question read as me wondering about that. – Jonathan W. Jan 13 at 23:57

Different approach altogether: put the brackets [ and ] into their own interchartok classes - and, when they are 'active', everything between them is in the same font:

MWE

\documentclass[varwidth,border=6pt]{standalone}
\usepackage{xcolor}
\usepackage{fontspec}
\defaultfontfeatures{Scale=MatchLowercase,Mapping=tex-text}
\newfontfeature{IPA}{+mgrk}
\setmainfont[IPA]{DejaVu Sans}
\newfontfamily\dejavuserif[IPA]{DejaVu Serif}[Colour=red]

% Define the opening-bracket class
\newXeTeXintercharclass\phopenclass
\XeTeXcharclass $= \phopenclass\relax % Define the closing-bracket class \newXeTeXintercharclass\phcloseclass % Add ] to it \XeTeXcharclass $ = \phcloseclass\relax

% When encountering an open bracket class, no need to insert any code at all.
%\XeTeXinterchartoks 0 \phopenclass {}
%\XeTeXinterchartoks 4095 \phopenclass {}

%When leaving an open bracket behind and encountering normal text or a boundary, switch on the phonetic font:
\XeTeXinterchartoks \phopenclass  4095 {\startphon}
\XeTeXinterchartoks \phopenclass 0 {\startphon}

%When encountering a closing bracket class, insert code to switch back to normal font:
\XeTeXinterchartoks 0 \phcloseclass {\finishphon}
\XeTeXinterchartoks 4095 \phcloseclass {\finishphon}

%When leaving a closing-bracket class, nothing to do: we are already back at normal font.
%\XeTeXinterchartoks \phcloseclass  4095 {}
%\XeTeXinterchartoks \phcloseclass 0 {}

\newcommand\startphon{\dejavuserif}
\newcommand\finishphon{\normalfont}
\XeTeXinterchartokenstate=1%switch on

\begin{document}

thaaw [tʰɑɑɯ] [tɑɑɯ] [tʰɑ́ɑɯ] [tɑ́ɑɯ] [tʰɑ́ɑɯʰ] thaaw

\XeTeXinterchartokenstate=0
These brackets [square ones] are [quite] normal text.

\XeTeXinterchartokenstate=1
Back to phonetic [pʰɶɴɛtɪk] brackets.

\end{document}