Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenquotes.git

Add tests for smart quotes

Author Dave Jarvis <email>
Date 2021-06-01 16:59:28 GMT-0700
Commit 6f119491ee1b004defe9d89e9af056a23139b3dc
Parent f40e32c
Delta 511 lines added, 111 lines removed, 400-line increase
lib/src/main/java/com/keenwrite/quotes/Contractions.java
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
package com.keenwrite.quotes;
import java.util.Set;
+/**
+ * Placeholder for various types of contractions.
+ * <p>
+ * TODO: Read from an external resource file.
+ * </p>
+ */
public class Contractions {
/**
lib/src/main/java/com/keenwrite/quotes/Lexeme.java
package com.keenwrite.quotes;
-import static com.keenwrite.quotes.LexemeType.INVALID;
+import static com.keenwrite.quotes.LexemeType.FLAG;
/**
- * Responsible for tracking the beginning and ending offsets of a token within
+ * Responsible for tracking the beginning and ending offsets of a lexeme within
* a text string. Tracking the beginning and ending indices should use less
* memory than duplicating the entire text of Unicode characters (i.e., using
* a similar approach to run-length encoding).
*/
public class Lexeme {
/**
- * Denotes there are no more tokens: the end of text (EOT) has been reached.
+ * Denotes there are no more lexemes: the end of text (EOT) has been reached.
*/
- public static final Lexeme EOT = new Lexeme();
+ public static final Lexeme EOT = new Lexeme( false );
+
+ /**
+ * Denotes parsing at the start of text. This is useful to avoid branching
+ * conditions while iterating.
+ */
+ public static final Lexeme SOT = new Lexeme( true );
private final LexemeType mType;
private final int mBegan;
private final int mEnded;
/**
- * Set to {@code true} if there are more tokens to parse.
+ * Set to {@code true} if there are more lexemes to parse.
*/
private final boolean mHasNext;
/**
- * Create an end of text token.
+ * Create a
*/
- private Lexeme() {
- this( INVALID, -1, -1, false );
+ private Lexeme( final boolean next ) {
+ this( FLAG, -1, -1, next );
}
/**
- * Create a token that indicates there are no more tokens.
+ * Create a lexeme that indicates there are no more lexemes.
*/
private Lexeme( final LexemeType type, final int began, final int ended ) {
this( type, began, ended, true );
}
/**
- * Create a token that represents a section of the text.
+ * Create a lexeme that represents a section of the text.
*/
private Lexeme(
/**
* Extracts a sequence of characters from the given text at the offsets
- * captured by this token.
+ * captured by this lexeme.
*
* @param text The text that was parsed using this class.
- * @return The character string captured by the token.
+ * @return The character string captured by the lexeme.
*/
public String toString( final String text ) {
static Lexeme createLexeme(
- final LexemeType token, final int began, final int ended ) {
- return new Lexeme( token, began, ended );
+ final LexemeType lexeme, final int began, final int ended ) {
+ return new Lexeme( lexeme, began, ended );
}
}
lib/src/main/java/com/keenwrite/quotes/LexemeType.java
PUNCT,
PERIOD,
- INVALID
+ FLAG
}
lib/src/main/java/com/keenwrite/quotes/Lexer.java
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
-import java.util.function.Consumer;
+import java.util.Iterator;
import static com.keenwrite.quotes.Lexeme.createLexeme;
import static com.keenwrite.quotes.LexemeType.*;
import static java.lang.Character.*;
import static java.text.CharacterIterator.DONE;
/**
* Turns text into words, numbers, punctuation, spaces, and more.
*/
-public class Lexer {
+public class Lexer implements Iterator<Lexeme> {
+
+ private final CharacterIterator mIterator;
+ private Lexeme mLexeme = Lexeme.SOT;
+
/**
* Default constructor, no state.
*/
- public Lexer() {
+ public Lexer( final String text ) {
+ mIterator = new StringCharacterIterator( text );
}
- /**
- * Emits a series of tokens that represent information about text that is
- * needed to convert straight quotes to curly quotes.
- *
- * @param text The text to split into tokens.
- * @param consumer Receives each token as a separate event.
- */
- public void parse( final String text, final Consumer<Lexeme> consumer ) {
- final var iterator = new StringCharacterIterator( text );
- Lexeme lex;
+ @Override
+ public boolean hasNext() {
+ return mLexeme.hasNext();
+ }
- while( (lex = parse( iterator )).hasNext() ) {
- consumer.accept( lex );
- }
+ @Override
+ public Lexeme next() {
+ return mLexeme = parse( mIterator );
}
lib/src/main/java/com/keenwrite/quotes/Parser.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
package com.keenwrite.quotes;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Consumer;
import static com.keenwrite.quotes.Contractions.beginsUnambiguously;
+import static com.keenwrite.quotes.Lexeme.EOT;
import static com.keenwrite.quotes.LexemeType.*;
import static com.keenwrite.quotes.TokenType.*;
* </ol>
*/
-public class Parser implements Consumer<Lexeme> {
+public class Parser {
+ /**
+ * The text to parse. A reference is required as a minor optimization in
+ * memory and speed: the lexer records integer offsets, rather than new
+ * {@link String} instances, to track parsed lexemes.
+ */
private final String mText;
- private final CircularFifoQueue<Lexeme> mTokens =
- new CircularFifoQueue<>( 3 );
- private final Deque<Lexeme> mStack = new ArrayDeque<>();
- private final Consumer<Token> mConsumer;
+ private final Lexer mLexer;
- public Parser( final String text, final Consumer<Token> consumer ) {
+ private final Deque<Lexeme> mQuotations = new ArrayDeque<>();
+ private final CircularFifoQueue<Lexeme> mLexemes =
+ new CircularFifoQueue<>( 3 );
+
+ public Parser( final String text ) {
mText = text;
+ mLexer = new Lexer( mText );
// Allow consuming the very first token without checking the queue size.
- mTokens.add( Lexeme.EOT );
- mTokens.add( Lexeme.EOT );
- mTokens.add( Lexeme.EOT );
-
- mConsumer = consumer;
+ flush( mLexemes );
}
- public void parse() {
- final var tokenizer = new Lexer();
- tokenizer.parse( mText, this );
- System.out.println(" DUH DONE!" );
+ /**
+ * Iterates over the entire text provided at construction, emitting
+ * {@link Token}s that can be used to convert straight quotes to curly
+ * quotes.
+ *
+ * @param consumer Receives emitted {@link Token}s.
+ */
+ public void parse( final Consumer<Token> consumer ) {
+ // Create/convert a list of all unambiguous quote characters.
+ while( mLexer.hasNext() ) {
+ parse( mLexer.next(), consumer );
+ }
+
+ // Create/convert a list of all unambiguous quotations.
+ // Let TERM ::= (, | ; | ! | ? | .)
+ // Find unambiguous quotations by searching for:
+ // ' WORD ('* SPACE+ WORD)* TERM '
+ // In other words, when a ' WORD is encountered, push the ' onto a stack.
+ // If ' WORD is encountered, pop the stack and push the new ' onto it.
+ // If TERM ' is encountered, push the new ' onto it.
+ // This algorithm may have to push " WORD and " TERM as well, to account
+ // for nested sentences.
+
+ // Convert remaining single quotes to apostrophes.
+
+
}
- @Override
- public void accept( final Lexeme token ) {
- mTokens.add( token );
+ private void parse( final Lexeme lexeme, final Consumer<Token> consumer ) {
+ mLexemes.add( lexeme );
- final var token1 = mTokens.get( 0 );
- final var token2 = mTokens.get( 1 );
- final var token3 = mTokens.get( 2 );
+ final var lex1 = mLexemes.get( 0 );
+ final var lex2 = mLexemes.get( 1 );
+ final var lex3 = mLexemes.get( 2 );
- if( token2.isType( QUOTE_SINGLE ) && token3.isType( WORD ) &&
- token1.anyType( WORD, PERIOD, NUMBER ) ) {
- mConsumer.accept( new Token( QUOTE_APOSTROPHE, token2 ) );
+ if( lex2.isType( QUOTE_SINGLE ) && lex3.isType( WORD ) &&
+ lex1.anyType( WORD, PERIOD, NUMBER ) ) {
+ consumer.accept( new Token( QUOTE_APOSTROPHE, lex2 ) );
+ flush( mLexemes );
}
- else if( token1.isType( QUOTE_SINGLE ) && token3.isType( QUOTE_SINGLE ) &&
- "n".equalsIgnoreCase( token2.toString( mText ) ) ) {
- mConsumer.accept( new Token( QUOTE_APOSTROPHE, token1 ) );
- mConsumer.accept( new Token( QUOTE_APOSTROPHE, token3 ) );
+ else if( lex1.isType( QUOTE_SINGLE ) && lex3.isType( QUOTE_SINGLE ) &&
+ "n".equalsIgnoreCase( lex2.toString( mText ) ) ) {
+ consumer.accept( new Token( QUOTE_APOSTROPHE, lex1 ) );
+ consumer.accept( new Token( QUOTE_APOSTROPHE, lex3 ) );
}
- else if( token1.isType( NUMBER ) && token2.isType( QUOTE_SINGLE ) ) {
- mConsumer.accept( new Token( QUOTE_PRIME_SINGLE, token2 ) );
+ else if( lex1.isType( NUMBER ) && lex2.isType( QUOTE_SINGLE ) ) {
+ consumer.accept( new Token( QUOTE_PRIME_SINGLE, lex2 ) );
}
- else if( token1.isType( NUMBER ) && token2.isType( QUOTE_DOUBLE ) ) {
- mConsumer.accept( new Token( QUOTE_PRIME_DOUBLE, token2 ) );
+ else if( lex1.isType( NUMBER ) && lex2.isType( QUOTE_DOUBLE ) ) {
+ consumer.accept( new Token( QUOTE_PRIME_DOUBLE, lex2 ) );
}
- else if( token1.isType( QUOTE_SINGLE ) && token2.isType( WORD ) &&
- beginsUnambiguously( token2.toString( mText ) ) ) {
- mConsumer.accept( new Token( QUOTE_APOSTROPHE, token1 ) );
+ else if( lex1.isType( QUOTE_SINGLE ) && lex2.isType( WORD ) &&
+ beginsUnambiguously( lex2.toString( mText ) ) ) {
+ consumer.accept( new Token( QUOTE_APOSTROPHE, lex1 ) );
}
- else if( token.anyType( QUOTE_SINGLE, QUOTE_DOUBLE ) ) {
- mStack.push( token );
-
- if( mStack.isEmpty() ) {
- System.out.println( "EMPTY STACK?!" );
- }
+ else if( lex1.anyType( QUOTE_SINGLE, QUOTE_DOUBLE ) ) {
+ mQuotations.push( lex1 );
+ System.out.println( "FOUND QUOTE: " + lex1 + " " + lex1.toString(mText));
+ }
+ else {
+ System.out.println( lex1 );
}
+ }
+
+ private void flush( final CircularFifoQueue<Lexeme> lexemes ) {
+ lexemes.add( Lexeme.SOT );
+ lexemes.add( Lexeme.SOT );
+ lexemes.add( Lexeme.SOT );
}
}
lib/src/main/java/com/keenwrite/quotes/SmartQuotes.java
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.quotes;
+
+/**
+ * Responsible for converting straight quotes into smart quotes.
+ */
+public class SmartQuotes {
+ public String replace( final String text ) {
+ final StringBuilder sb = new StringBuilder( text );
+
+ final var parser = new Parser( text );
+ parser.parse( (token) -> {
+
+ } );
+
+ return sb.toString();
+ }
+}
lib/src/main/java/com/keenwrite/quotes/Token.java
}
+ TokenType getType() {
+ return mType;
+ }
+
+ public String toString( final String text ) {
+ return mLexeme.toString( text );
+ }
+
@Override
public String toString() {
lib/src/test/java/com/keenwrite/quotes/LexerTest.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
package com.keenwrite.quotes;
import org.junit.jupiter.api.Test;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiFunction;
import static com.keenwrite.quotes.LexemeType.*;
testType( "abc \r\nabc\n", WORD, SPACE, NEWLINE, WORD, NEWLINE );
}
-
- private void testType(
- final String actual, final LexemeType... expected ) {
- final var tokenizer = new Lexer();
- final var counter = new AtomicInteger();
-
- tokenizer.parse( actual, ( token ) -> {
- final var expectedType = expected[ counter.getAndIncrement() ];
- final var actualType = token.getType();
- assertEquals( expectedType, actualType );
- } );
- // Ensure all expected tokens are matched (verify end of text reached).
- assertEquals( expected.length, counter.get() );
+ private void testType( final String actual, final LexemeType... expected ) {
+ testType(
+ actual, ( lexeme, text ) -> lexeme.getType(), Arrays.asList( expected ) );
}
private void testText( final String actual, final String... expected ) {
- final var tokenizer = new Lexer();
- final var counter = new AtomicInteger();
+ testType( actual, Lexeme::toString, Arrays.asList( expected ) );
+ }
- tokenizer.parse( actual, ( token ) -> {
- final var expectedText = expected[ counter.getAndIncrement() ];
- final var actualText = token.toString( actual );
- assertEquals( expectedText, actualText );
- } );
+ private <A, E> void testType(
+ final String text,
+ final BiFunction<Lexeme, String, A> f,
+ final List<E> elements ) {
+ final var lexer = new Lexer( text );
+ var counter = 0;
- // Ensure all expected tokens are matched (verify end of text reached).
- assertEquals( expected.length, counter.get() );
+ do {
+ final var lexeme = lexer.next();
+ final var expected = elements.get( counter++ );
+ final var actual = f.apply( lexeme, text );
+ assertEquals( expected, actual );
+ }
+ while( lexer.hasNext() );
+
+ // Ensure all expected values are matched (verify end of text reached).
+ assertEquals( elements.size(), counter );
}
}
lib/src/test/java/com/keenwrite/quotes/ParserTest.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
package com.keenwrite.quotes;
import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.keenwrite.quotes.TokenType.QUOTE_APOSTROPHE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
class ParserTest {
- @Test
- void test_Conversion_Straight_Curly() {
- parse( "\"It's the 70's jack-o'-lantern\"" );
- parse( "Fish-'n'-chips!" );
- parse( "That's a 35' x 10\" yacht!" );
- parse( "He's a kinda' cat ya'll couldn't've known!" );
- parse( "'70s are Sams' faves.'" );
- parse( "'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx." );
- parse( """
+ private final static Map<String, Map<TokenType, Integer>> TEST_CASES =
+ Map.of(
+// "It's the 80's...",
+// Map.of( QUOTE_APOSTROPHE, 2 ),
+// "Fish-'n'-chips!",
+// Map.of( QUOTE_APOSTROPHE, 2 ),
+// "That's a 35'×10\" yacht!",
+// Map.of( QUOTE_APOSTROPHE, 1 ),
+// "She's a cat ya'll couldn't've known!",
+// Map.of( QUOTE_APOSTROPHE, 4 ),
+// "'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx.",
+// Map.of( QUOTE_APOSTROPHE, 5 ),
+ """
But I must leave the proofs to those who 've seen 'em;
But this I heard her say, and can't be wrong
And all may think which way their judgments lean 'em,
''T is strange—the Hebrew noun which means "I am,"
The English always use to govern d--n.'
- """ );
+ """,
+ Map.of( QUOTE_APOSTROPHE, 5 )
+ );
+
+ @Test
+ void test_Conversion_Straight_Curly() {
+ for( final var entry : TEST_CASES.entrySet() ) {
+ parse( entry.getKey(), entry.getValue() );
+ }
}
- private void parse( final String text ) {
- final var parser = new Parser( text, System.out::println );
- parser.parse();
+ private void parse( final String text, final Map<TokenType, Integer> tally ) {
+ final var parser = new Parser( text );
+ final var actual = new HashMap<TokenType, Integer>();
+
+ parser.parse(
+ ( token ) -> actual.merge( token.getType(), 1, Integer::sum )
+ );
+
+ for( final var expectedEntry : tally.entrySet() ) {
+ final var expectedCount = expectedEntry.getValue();
+ final var actualCount = actual.get( expectedEntry.getKey() );
+
+ assertEquals( expectedCount, actualCount, text );
+ }
}
}
lib/src/test/java/com/keenwrite/quotes/SmartQuotesTest.java
+/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.quotes;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Test that English straight quotes are converted to curly quotes and
+ * apostrophes.
+ */
+public class SmartQuotesTest {
+ @Test
+ public void test_Parse_StraightQuotes_CurlyQuotes() throws IOException {
+ final var fixer = new SmartQuotes();
+
+ try( final var reader = openResource( "smartypants.txt" ) ) {
+ String line;
+ String testLine = "";
+ String expected = "";
+
+ while( ((line = reader.readLine()) != null) ) {
+ if( line.startsWith( "#" ) || line.isBlank() ) { continue; }
+
+ // Read the first line of the couplet.
+ if( testLine.isBlank() ) {
+ testLine = line;
+ continue;
+ }
+
+ // Read the second line of the couplet.
+ if( expected.isBlank() ) {
+ expected = line;
+ }
+
+ final var actual = fixer.replace( testLine );
+ assertEquals(expected, actual);
+
+ testLine = "";
+ expected = "";
+ }
+ }
+ }
+
+ @SuppressWarnings( "SameParameterValue" )
+ private BufferedReader openResource( final String filename ) {
+ final var is = getClass().getResourceAsStream( filename );
+ assertNotNull( is );
+
+ return new BufferedReader( new InputStreamReader( is ) );
+ }
+}
lib/src/test/resources/com.keenwrite.quotes/smartypants.txt
+# ########################################################################
+# Primes (single, double)
+# ########################################################################
+She stood 5\'7\".
+She stood 5&prime;7&Prime;.
+
+# No space after the feet sign.
+It's 4'11" away.
+It&apos;s 4&prime;11&Prime; away.
+
+Alice's friend is 6'3" tall.
+Alice&apos;s friend is 6&prime;3&Prime; tall.
+
+Bob's table is 5'' × 4''.
+Bob&apos;s table is 5&Prime; × 4&Prime;.
+
+Bob's table is 5'×4'.
+Bob&apos;s table is 5&prime;×4&prime;.
+
+What's this -5.5'' all about?
+What&apos;s this -5.5&Prime; all about?
+
++7.9'' is weird.
++7.9&Prime; is weird.
+
+Foolscap? Naw, I use 11.5"x14.25" paper!
+Foolscap? Naw, I use 11.5&Prime;x14.25&Prime; paper!
+
+An angular measurement, 3° 5' 30" means 3 degs, 5 arcmins, and 30 arcsecs.
+An angular measurement, 3° 5&prime; 30&Prime; means 3 degs, 5 arcmins, and 30 arcsecs.
+
+# ########################################################################
+# Inside contractions (no leading/trailing apostrophes)
+# ########################################################################
+I don't like it: I love's it!
+I don&apos;t like it: I love&apos;s it!
+
+We'd've thought that pancakes'll be sweeter there.
+We&apos;d&apos;ve thought that pancakes&apos;ll be sweeter there.
+
+She'd be coming o'er when the horse'd gone to pasture...
+She&apos;d be coming o&apos;er when the horse&apos;d gone to pasture...
+
+# ########################################################################
+# Beginning contractions (leading apostrophes)
+# ########################################################################
+'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx.
+&apos;Twas and &apos;tis whate&apos;er lay &apos;twixt dawn and dusk &apos;n River Styx.
+
+# ########################################################################
+# Outside contractions (leading and trailing, no middle)
+# ########################################################################
+Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice!
+Salt &apos;n&apos; vinegar, fish-&apos;n&apos;-chips, sugar &apos;n&apos; spice!
+
+# ########################################################################
+# Ending contractions (trailing apostrophes)
+# ########################################################################
+Didn' get th' message.
+Didn&apos; get th&apos; message.
+
+Namsayin', y'know what I'ma sayin'?
+Namsayin&apos;, y&apos;know what I&apos;ma sayin&apos;?
+
+# ########################################################################
+# Decades
+# ########################################################################
+The Roaring '20s had the best music, no?
+The Roaring &apos;20s had the best music, no?
+
+Took place in '04, yes'm!
+Took place in &apos;04, yes&apos;m!
+
+# ########################################################################
+# Backticks (left and right double quotes)
+# ########################################################################
+``I am Sam''
+&ldquo;I am Sam&rdquo;
+
+``Sam's away today''
+&ldquo;Sam&apos;s away today&rdquo;
+
+``Sam's gone!
+&ldquo;Sam&apos;s gone!
+
+``5'10" tall 'e was!''
+&ldquo;5&prime;10&Prime; tall &apos;e was!&rdquo;
+
+# ########################################################################
+# Consecutive quotes
+# ########################################################################
+"'I'm trouble.'"
+&ldquo;&lsquo;I&apos;m trouble.&rsquo;&rdquo;
+
+'"Trouble's my name."'
+&lsquo;&ldquo;Trouble&apos;s my name.&ldquo;&lsquo;
+
+# ########################################################################
+# Escaped quotes
+# ########################################################################
+\"What?\"
+&ldquo;What?&rdquo;
+
+# ########################################################################
+# Double quotes
+# ########################################################################
+"I am Sam"
+&ldquo;I am Sam&rdquo;
+
+"...even better!"
+&ldquo;...even better!&rdquo;
+
+"It was so," said he.
+&ldquo;It was so,&rdquo; said he.
+
+"She said, 'Llamas'll languish, they'll--
+&ldquo;She said, &lsquo;Llamas&apos;ll languish, they&apos;ll--
+
+With "air quotes" in the middle.
+With &ldquo;air quotes&rdquo; in the middle.
+
+With--"air quotes"--and dashes.
+With--&ldquo;air quotes&rdquo;--and dashes.
+
+"Not "quite" what you expected?"
+&ldquo;Not &ldquo;quite&rdquo; what you expected?&rdquo;
+
+# ########################################################################
+# Nested quotations
+# ########################################################################
+"'Here I am,' said Sam"
+&ldquo;&lsquo;Here I am,&rsquo; said Sam&rdquo;
+
+'"Here I am," said Sam'
+&lsquo;&ldquo;Here I am,&rdquo;, said Sam&rsquo;
+
+'Hello, "Dr. Brown," what's your real name?'
+&lsquo;Hello, &ldquo;Dr. Brown,&rdquo; what's your real name?&rsquo;
+
+"'Twas, t'wasn't thy name, 'twas it?" said Jim "the Barber" Brown.
+&ldquo;&apos;Twas, t&apos;wasn&apos;t thy name, &apos;twas it?&rdquo; said Jim &ldquo;the Barber&rdquo; Brown.
+
+# ########################################################################
+# Single quotes
+# ########################################################################
+'I am Sam'
+&lsquo;I am Sam&rsquo;
+
+'It was so,' said he.
+&lsquo;It was so,&rsquo; said he.
+
+'...even better!'
+&lsquo;...even better!&rsquo;
+
+With 'quotes' in the middle.
+With &lsquo;quotes&rsquo; in the middle.
+
+With--'imaginary'--dashes.
+With--&lsquo;imaginary&rsquo;--dashes.
+
+'Not 'quite' what you expected?'
+&lsquo;Not &lsquo;quite&rsquo; what you expected?&rsquo;
+
+''Cause I don't like it, 's why,' said Pat.
+&lsquo;&apos;Cause I don't like it, &apos;s why,&rsquo; said Pat.
+
+'It's a beautiful day!'
+&lsquo;It&apos;s a beautiful day!&rsquo;
+
+'He said, "Thinkin'."'
+&lsquo;He said, &ldquo;Thinkin&rsquo;.&rdquo;&rsquo;
+
+# ########################################################################
+# Possessives
+# ########################################################################
+Sam's Sams' and the Ross's roses' thorns were prickly.
+Sam&apos;s Sams&apos; and the Ross&apos;s roses&apos; thorns were prickly.
+
+# ########################################################################
+# Mixed
+# ########################################################################
+"I heard she said, 'That's Sam's'," said the Sams' cat.
+&ldquo;I heard she said, &lsquo;That&apos;s Sam&apos;s&rsquo;,&rdquo; said the Sams&apos; cat.
+
+"'Janes' said, ''E'll be spooky, Sam's son with the jack-o'-lantern!'" said the O'Mally twins'---y'know---ghosts in unison.
+&ldquo;&lsquo;Janes&apos; said, &lsquo;&apos;E&apos;ll be spooky, Sam&apos;s son with the jack-o&apos;-lantern!&rsquo;&rdquo; said the O&apos;Mally twins&apos;---y&apos;know---ghosts in unison.
+
+'He's at Sams'
+&lsquo;He&apos; at Sams&rsquo;
+
+\"Hello!\"
+&ldquo;Hello!&rdquo;
+
+ma'am
+ma&apos;am
+
+'Twas midnight
+&apos;Twas midnight
+
+\"Hello,\" said the spider. \"'Shelob' is my name.\"
+&ldquo;Hello,&rdquo; said the spider. &ldquo;&lsquo;Shelob&rsquo; is my name.&rdquo;
+
+'A', 'B', and 'C' are letters.
+&lsquo;A&rsquo; &lsquo;B&rsquo; and &lsquo;C&rsquo; are letters.
+
+'Oak,' 'elm,' and 'beech' are names of trees. So is 'pine.'
+&lsquo;Oak,&rsquo; &lsquo;elm,&rsquo; and &lsquo;beech&rsquo; are names of trees. So is &lsquo;pine.&rsquo;
+
+'He said, \"I want to go.\"' Were you alive in the 70's?
+&lsquo;He said, &ldquo;I want to go.&rdquo;&rsquo; Were you alive in the 70&apos;s?
+
+\"That's a 'magic' sock.\"
+&ldquo;That&apos;s a &lsquo;magic&rsquo; sock.&rdquo;
+
+Website! Company Name, Inc. (\"Company Name\" or \"Company\") recommends reading the following terms and conditions, carefully:
+Website! Company Name, Inc. (&ldquo;Company Name&rdquo; or &ldquo;Company&rdquo;) recommends reading the following terms and conditions, carefully:
+
+Website! Company Name, Inc. ('Company Name' or 'Company') recommends reading the following terms and conditions, carefully:
+Website! Company Name, Inc. (&lsquo;Company Name&rsquo; or &lsquo;Company&rsquo;) recommends reading the following terms and conditions, carefully:
+
+Workin' hard
+Workin&apos; hard
+
+'70s are my favorite numbers,' she said.
+&lsquo;70s are my favorite numbers,&rsquo; she said.
+
+'70s fashion was weird.
+&apos;70s fashion was weird.
+
+12\" record, 5'10\" height
+12&Prime; record, 5&prime;10&Prime; height
+
+Model \"T2000\"
+Model &ldquo;T2000&rdquo;
+
+iPad 3's battery life is not great.
+iPad 3&apos;s battery life is not great.
+
+Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season.
+Book &apos;em, Danno. Rock &apos;n&apos; roll. &apos;Cause &apos;twas the season.
+
+'85 was a good year. (The entire '80s were.)
+&apos;85 was a good year. (The entire &apos;80s were.)