| 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 |
|---|
| +/* 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 { | ||
| /** |
| 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 ); | ||
| } | ||
| } | ||
| PUNCT, | ||
| PERIOD, | ||
| - INVALID | ||
| + FLAG | ||
| } | ||
| 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 ); | ||
| } | ||
| -/* 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 ); | ||
| } | ||
| } | ||
| +/* 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(); | ||
| + } | ||
| +} | ||
| } | ||
| + TokenType getType() { | ||
| + return mType; | ||
| + } | ||
| + | ||
| + public String toString( final String text ) { | ||
| + return mLexeme.toString( text ); | ||
| + } | ||
| + | ||
| @Override | ||
| public String toString() { |
| -/* 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 ); | ||
| } | ||
| } | ||
| -/* 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 ); | ||
| + } | ||
| } | ||
| } |
| +/* 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 ) ); | ||
| + } | ||
| +} | ||
| +# ######################################################################## | ||
| +# Primes (single, double) | ||
| +# ######################################################################## | ||
| +She stood 5\'7\". | ||
| +She stood 5′7″. | ||
| + | ||
| +# No space after the feet sign. | ||
| +It's 4'11" away. | ||
| +It's 4′11″ away. | ||
| + | ||
| +Alice's friend is 6'3" tall. | ||
| +Alice's friend is 6′3″ tall. | ||
| + | ||
| +Bob's table is 5'' × 4''. | ||
| +Bob's table is 5″ × 4″. | ||
| + | ||
| +Bob's table is 5'×4'. | ||
| +Bob's table is 5′×4′. | ||
| + | ||
| +What's this -5.5'' all about? | ||
| +What's this -5.5″ all about? | ||
| + | ||
| ++7.9'' is weird. | ||
| ++7.9″ is weird. | ||
| + | ||
| +Foolscap? Naw, I use 11.5"x14.25" paper! | ||
| +Foolscap? Naw, I use 11.5″x14.25″ paper! | ||
| + | ||
| +An angular measurement, 3° 5' 30" means 3 degs, 5 arcmins, and 30 arcsecs. | ||
| +An angular measurement, 3° 5′ 30″ 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't like it: I love's it! | ||
| + | ||
| +We'd've thought that pancakes'll be sweeter there. | ||
| +We'd've thought that pancakes'll be sweeter there. | ||
| + | ||
| +She'd be coming o'er when the horse'd gone to pasture... | ||
| +She'd be coming o'er when the horse'd gone to pasture... | ||
| + | ||
| +# ######################################################################## | ||
| +# Beginning contractions (leading apostrophes) | ||
| +# ######################################################################## | ||
| +'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx. | ||
| +'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx. | ||
| + | ||
| +# ######################################################################## | ||
| +# Outside contractions (leading and trailing, no middle) | ||
| +# ######################################################################## | ||
| +Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice! | ||
| +Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice! | ||
| + | ||
| +# ######################################################################## | ||
| +# Ending contractions (trailing apostrophes) | ||
| +# ######################################################################## | ||
| +Didn' get th' message. | ||
| +Didn' get th' message. | ||
| + | ||
| +Namsayin', y'know what I'ma sayin'? | ||
| +Namsayin', y'know what I'ma sayin'? | ||
| + | ||
| +# ######################################################################## | ||
| +# Decades | ||
| +# ######################################################################## | ||
| +The Roaring '20s had the best music, no? | ||
| +The Roaring '20s had the best music, no? | ||
| + | ||
| +Took place in '04, yes'm! | ||
| +Took place in '04, yes'm! | ||
| + | ||
| +# ######################################################################## | ||
| +# Backticks (left and right double quotes) | ||
| +# ######################################################################## | ||
| +``I am Sam'' | ||
| +“I am Sam” | ||
| + | ||
| +``Sam's away today'' | ||
| +“Sam's away today” | ||
| + | ||
| +``Sam's gone! | ||
| +“Sam's gone! | ||
| + | ||
| +``5'10" tall 'e was!'' | ||
| +“5′10″ tall 'e was!” | ||
| + | ||
| +# ######################################################################## | ||
| +# Consecutive quotes | ||
| +# ######################################################################## | ||
| +"'I'm trouble.'" | ||
| +“‘I'm trouble.’” | ||
| + | ||
| +'"Trouble's my name."' | ||
| +‘“Trouble's my name.“‘ | ||
| + | ||
| +# ######################################################################## | ||
| +# Escaped quotes | ||
| +# ######################################################################## | ||
| +\"What?\" | ||
| +“What?” | ||
| + | ||
| +# ######################################################################## | ||
| +# Double quotes | ||
| +# ######################################################################## | ||
| +"I am Sam" | ||
| +“I am Sam” | ||
| + | ||
| +"...even better!" | ||
| +“...even better!” | ||
| + | ||
| +"It was so," said he. | ||
| +“It was so,” said he. | ||
| + | ||
| +"She said, 'Llamas'll languish, they'll-- | ||
| +“She said, ‘Llamas'll languish, they'll-- | ||
| + | ||
| +With "air quotes" in the middle. | ||
| +With “air quotes” in the middle. | ||
| + | ||
| +With--"air quotes"--and dashes. | ||
| +With--“air quotes”--and dashes. | ||
| + | ||
| +"Not "quite" what you expected?" | ||
| +“Not “quite” what you expected?” | ||
| + | ||
| +# ######################################################################## | ||
| +# Nested quotations | ||
| +# ######################################################################## | ||
| +"'Here I am,' said Sam" | ||
| +“‘Here I am,’ said Sam” | ||
| + | ||
| +'"Here I am," said Sam' | ||
| +‘“Here I am,”, said Sam’ | ||
| + | ||
| +'Hello, "Dr. Brown," what's your real name?' | ||
| +‘Hello, “Dr. Brown,” what's your real name?’ | ||
| + | ||
| +"'Twas, t'wasn't thy name, 'twas it?" said Jim "the Barber" Brown. | ||
| +“'Twas, t'wasn't thy name, 'twas it?” said Jim “the Barber” Brown. | ||
| + | ||
| +# ######################################################################## | ||
| +# Single quotes | ||
| +# ######################################################################## | ||
| +'I am Sam' | ||
| +‘I am Sam’ | ||
| + | ||
| +'It was so,' said he. | ||
| +‘It was so,’ said he. | ||
| + | ||
| +'...even better!' | ||
| +‘...even better!’ | ||
| + | ||
| +With 'quotes' in the middle. | ||
| +With ‘quotes’ in the middle. | ||
| + | ||
| +With--'imaginary'--dashes. | ||
| +With--‘imaginary’--dashes. | ||
| + | ||
| +'Not 'quite' what you expected?' | ||
| +‘Not ‘quite’ what you expected?’ | ||
| + | ||
| +''Cause I don't like it, 's why,' said Pat. | ||
| +‘'Cause I don't like it, 's why,’ said Pat. | ||
| + | ||
| +'It's a beautiful day!' | ||
| +‘It's a beautiful day!’ | ||
| + | ||
| +'He said, "Thinkin'."' | ||
| +‘He said, “Thinkin’.”’ | ||
| + | ||
| +# ######################################################################## | ||
| +# Possessives | ||
| +# ######################################################################## | ||
| +Sam's Sams' and the Ross's roses' thorns were prickly. | ||
| +Sam's Sams' and the Ross's roses' thorns were prickly. | ||
| + | ||
| +# ######################################################################## | ||
| +# Mixed | ||
| +# ######################################################################## | ||
| +"I heard she said, 'That's Sam's'," said the Sams' cat. | ||
| +“I heard she said, ‘That's Sam's’,” said the Sams' 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. | ||
| +“‘Janes' said, ‘'E'll be spooky, Sam's son with the jack-o'-lantern!’” said the O'Mally twins'---y'know---ghosts in unison. | ||
| + | ||
| +'He's at Sams' | ||
| +‘He' at Sams’ | ||
| + | ||
| +\"Hello!\" | ||
| +“Hello!” | ||
| + | ||
| +ma'am | ||
| +ma'am | ||
| + | ||
| +'Twas midnight | ||
| +'Twas midnight | ||
| + | ||
| +\"Hello,\" said the spider. \"'Shelob' is my name.\" | ||
| +“Hello,” said the spider. “‘Shelob’ is my name.” | ||
| + | ||
| +'A', 'B', and 'C' are letters. | ||
| +‘A’ ‘B’ and ‘C’ are letters. | ||
| + | ||
| +'Oak,' 'elm,' and 'beech' are names of trees. So is 'pine.' | ||
| +‘Oak,’ ‘elm,’ and ‘beech’ are names of trees. So is ‘pine.’ | ||
| + | ||
| +'He said, \"I want to go.\"' Were you alive in the 70's? | ||
| +‘He said, “I want to go.”’ Were you alive in the 70's? | ||
| + | ||
| +\"That's a 'magic' sock.\" | ||
| +“That's a ‘magic’ sock.” | ||
| + | ||
| +Website! Company Name, Inc. (\"Company Name\" or \"Company\") 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. ('Company Name' or 'Company') recommends reading the following terms and conditions, carefully: | ||
| +Website! Company Name, Inc. (‘Company Name’ or ‘Company’) recommends reading the following terms and conditions, carefully: | ||
| + | ||
| +Workin' hard | ||
| +Workin' hard | ||
| + | ||
| +'70s are my favorite numbers,' she said. | ||
| +‘70s are my favorite numbers,’ she said. | ||
| + | ||
| +'70s fashion was weird. | ||
| +'70s fashion was weird. | ||
| + | ||
| +12\" record, 5'10\" height | ||
| +12″ record, 5′10″ height | ||
| + | ||
| +Model \"T2000\" | ||
| +Model “T2000” | ||
| + | ||
| +iPad 3's battery life is not great. | ||
| +iPad 3's battery life is not great. | ||
| + | ||
| +Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season. | ||
| +Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season. | ||
| + | ||
| +'85 was a good year. (The entire '80s were.) | ||
| +'85 was a good year. (The entire '80s were.) | ||