Skip to content

Commit

Permalink
Paired punctuation checking initial implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben King committed Dec 5, 2024
1 parent d6a7223 commit ef0d0dc
Show file tree
Hide file tree
Showing 17 changed files with 2,220 additions and 369 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { PairedPunctuationRule } from '../rule-set/rule-utils';
import { CharacterClassRegexBuilder } from '../utils';

export class PairedPunctuationConfig {
private readonly standardRules: PairedPunctuationRule[] = [];
private readonly quotationRules: PairedPunctuationRule[] = [];
private openingMarkRegex = /[([{]/;
private closingMarkRegex = /[)}\]]/;
private correspondingMarkMap: Map<string, string> = new Map<string, string>();
private quotationMarks: Set<string> = new Set<string>();

// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}

private intializeAfterBuilding(): void {
this.openingMarkRegex = this.createOpeningMarkRegex();
this.closingMarkRegex = this.createClosingMarkRegex();
this.correspondingMarkMap = this.createCorrespondingMarkMap();
this.quotationMarks = this.createQuotationMarkSet();
}

private createOpeningMarkRegex(): RegExp {
const quoteRegexBuilder: CharacterClassRegexBuilder = new CharacterClassRegexBuilder();
for (const punctuationPair of this.standardRules) {
quoteRegexBuilder.addCharacter(punctuationPair.openingPunctuationMark);
}
for (const punctuationPair of this.quotationRules) {
quoteRegexBuilder.addCharacter(punctuationPair.openingPunctuationMark);
}
return quoteRegexBuilder.build();
}

private createClosingMarkRegex(): RegExp {
const quoteRegexBuilder: CharacterClassRegexBuilder = new CharacterClassRegexBuilder();
for (const punctuationPair of this.standardRules) {
quoteRegexBuilder.addCharacter(punctuationPair.closingPunctuationMark);
}
for (const punctuationPair of this.quotationRules) {
quoteRegexBuilder.addCharacter(punctuationPair.closingPunctuationMark);
}
return quoteRegexBuilder.build();
}

private createCorrespondingMarkMap(): Map<string, string> {
const correspondingMarkMap: Map<string, string> = new Map<string, string>();
for (const rule of this.standardRules) {
correspondingMarkMap.set(rule.openingPunctuationMark, rule.closingPunctuationMark);
correspondingMarkMap.set(rule.closingPunctuationMark, rule.openingPunctuationMark);
}
for (const rule of this.quotationRules) {
correspondingMarkMap.set(rule.openingPunctuationMark, rule.closingPunctuationMark);
correspondingMarkMap.set(rule.closingPunctuationMark, rule.openingPunctuationMark);
}
return correspondingMarkMap;
}

private createQuotationMarkSet(): Set<string> {
const quotationMarkSet: Set<string> = new Set<string>();
for (const rule of this.quotationRules) {
quotationMarkSet.add(rule.openingPunctuationMark);
quotationMarkSet.add(rule.closingPunctuationMark);
}
return quotationMarkSet;
}

public createAllPairedMarksRegex(): RegExp {
const quoteRegexBuilder: CharacterClassRegexBuilder = new CharacterClassRegexBuilder();
for (const punctuationPair of this.standardRules) {
quoteRegexBuilder
.addCharacter(punctuationPair.openingPunctuationMark)
.addCharacter(punctuationPair.closingPunctuationMark);
}
for (const punctuationPair of this.quotationRules) {
quoteRegexBuilder
.addCharacter(punctuationPair.openingPunctuationMark)
.addCharacter(punctuationPair.closingPunctuationMark);
}
return quoteRegexBuilder.makeGlobal().build();
}

public isOpeningMark(punctuationMark: string): boolean {
return this.openingMarkRegex.test(punctuationMark);
}

public isClosingMark(punctuationMark: string): boolean {
return this.closingMarkRegex.test(punctuationMark);
}

public doMarksConstituteAPair(openingMark: string, closingMark: string): boolean {
return this.correspondingMarkMap.get(openingMark) === closingMark;
}

public findCorrespondingMark(punctuationMark: string): string | undefined {
if (this.correspondingMarkMap.has(punctuationMark)) {
return this.correspondingMarkMap.get(punctuationMark);
}
return undefined;
}

public shouldErrorForUnmatchedMarks(punctuationMark: string): boolean {
return !this.quotationMarks.has(punctuationMark);
}

public static Builder = class {
pairedPunctuationConfig: PairedPunctuationConfig = new PairedPunctuationConfig();

public addRule(pairedPunctuationRule: PairedPunctuationRule): this {
this.pairedPunctuationConfig.standardRules.push(pairedPunctuationRule);
return this;
}

public addQuotationRule(pairedPunctuationRule: PairedPunctuationRule): this {
this.pairedPunctuationConfig.quotationRules.push(pairedPunctuationRule);
return this;
}

public build(): PairedPunctuationConfig {
this.pairedPunctuationConfig.intializeAfterBuilding();
return this.pairedPunctuationConfig;
}
};
}
28 changes: 15 additions & 13 deletions packages/punctuation-checker/src/quotation/quotation-analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PairedPunctuationDirection } from '../utils';
import { QuotationConfig } from './quotation-config';
import {
QuotationDepth,
QuotationDirection,
QuotationIterator,
QuotationRootLevel,
QuoteCorrection,
Expand Down Expand Up @@ -48,9 +48,9 @@ export class QuotationAnalyzer {
}

private processQuotationMarkByDirection(quotationMark: QuoteMetadata) {
if (quotationMark.direction === QuotationDirection.Opening) {
if (quotationMark.direction === PairedPunctuationDirection.Opening) {
this.processOpeningQuotationMark(quotationMark);
} else if (quotationMark.direction === QuotationDirection.Closing) {
} else if (quotationMark.direction === PairedPunctuationDirection.Closing) {
this.processClosingQuotationMark(quotationMark);
}
}
Expand Down Expand Up @@ -147,13 +147,13 @@ class QuotationResolver {

private triviallyResolve(unresolvedQuotationMark: UnresolvedQuoteMetadata): QuoteMetadata {
const chosenDepth: QuotationDepth = unresolvedQuotationMark.findBestDepth(() => 1);
const chosenDirection: QuotationDirection = unresolvedQuotationMark.findBestDirection(() => 1);
const chosenDirection: PairedPunctuationDirection = unresolvedQuotationMark.findBestDirection(() => 1);
return unresolvedQuotationMark.resolve(chosenDepth, chosenDirection);
}

private resolveTopLevelOpeningQuoteIfPossible(unresolvedQuotationMark: UnresolvedQuoteMetadata): QuoteMetadata {
return this.resolveWithPreferences(unresolvedQuotationMark, [
{ direction: QuotationDirection.Opening, depth: QuotationDepth.Primary },
{ direction: PairedPunctuationDirection.Opening, depth: QuotationDepth.Primary },
]);
}

Expand All @@ -166,7 +166,7 @@ class QuotationResolver {
return unresolvedQuotationMark.resolve(preference.depth, preference.direction);
}
}
const bestDefaultDirection: QuotationDirection = this.chooseBestDefaultDirection(unresolvedQuotationMark);
const bestDefaultDirection: PairedPunctuationDirection = this.chooseBestDefaultDirection(unresolvedQuotationMark);
const bestDefaultDepth: QuotationDepth = this.chooseBestDefaultDepth(unresolvedQuotationMark, bestDefaultDirection);
return unresolvedQuotationMark.resolve(bestDefaultDepth, bestDefaultDirection);
}
Expand All @@ -184,17 +184,19 @@ class QuotationResolver {
// If we get to the default functions, we've already checked that
// it can't be a legal closing quote, so we assume that the user is
// more likely to have meant to open a new quote
private chooseBestDefaultDirection(unresolvedQuotationMark: UnresolvedQuoteMetadata): QuotationDirection {
return unresolvedQuotationMark.findBestDirection((direction) => (direction === QuotationDirection.Opening ? 1 : 0));
private chooseBestDefaultDirection(unresolvedQuotationMark: UnresolvedQuoteMetadata): PairedPunctuationDirection {
return unresolvedQuotationMark.findBestDirection((direction) =>
direction === PairedPunctuationDirection.Opening ? 1 : 0,
);
}

private chooseBestDefaultDepth(
unresolvedQuotationMark: UnresolvedQuoteMetadata,
chosenDirection: QuotationDirection,
chosenDirection: PairedPunctuationDirection,
): QuotationDepth {
// Choose a shallower depth for a closing quote and deeper for an opening quote,
// but as close the ideal depth as possible
if (chosenDirection === QuotationDirection.Opening) {
if (chosenDirection === PairedPunctuationDirection.Opening) {
const idealDepth: QuotationDepth = this.deepestOpenQuote?.depth.deeper() ?? QuotationDepth.Primary;
return unresolvedQuotationMark.findBestDepth(
(depth) =>
Expand All @@ -212,15 +214,15 @@ class QuotationResolver {
// this function is only called if deepestOpenQuote is not null
private resolveAllowedQuoteIfPossible(unresolvedQuotationMark: UnresolvedQuoteMetadata): QuoteMetadata {
return this.resolveWithPreferences(unresolvedQuotationMark, [
{ direction: QuotationDirection.Closing, depth: this.deepestOpenQuote!.depth },
{ direction: QuotationDirection.Opening, depth: this.deepestOpenQuote!.depth.deeper() },
{ direction: PairedPunctuationDirection.Closing, depth: this.deepestOpenQuote!.depth },
{ direction: PairedPunctuationDirection.Opening, depth: this.deepestOpenQuote!.depth.deeper() },
]);
}
}

interface DepthAndDirectionPreference {
depth: QuotationDepth;
direction: QuotationDirection;
direction: PairedPunctuationDirection;
}

export class QuotationAnalysis {
Expand Down
14 changes: 9 additions & 5 deletions packages/punctuation-checker/src/quotation/quotation-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { AbstractChecker } from '../abstract-checker';
import { DiagnosticFactory } from '../diagnostic-factory';
import { DiagnosticList } from '../diagnostic-list';
import { StandardFixes } from '../standard-fixes';
import { PairedPunctuationDirection } from '../utils';
import { QuotationAnalysis, QuotationAnalyzer } from './quotation-analyzer';
import { QuotationConfig } from './quotation-config';
import { QuotationDepth, QuotationDirection, QuoteCorrection, QuoteMetadata } from './quotation-utils';
import { QuotationDepth, QuoteCorrection, QuoteMetadata } from './quotation-utils';

const UNMATCHED_OPENING_QUOTE_DIAGNOSTIC_CODE = 'unmatched-opening-quotation-mark';
const UNMATCHED_OPENING_QUOTE_MESSAGE = 'Opening quotation mark with no closing mark.';
Expand Down Expand Up @@ -77,10 +78,13 @@ export class QuotationChecker extends AbstractChecker {
const expectedQuotationDepth = Number(match[1]) + 1;
return this.quotationConfig.getUnambiguousQuotationMarkByType(
QuotationDepth.fromNumber(expectedQuotationDepth),
QuotationDirection.Opening,
PairedPunctuationDirection.Opening,
);
}
return this.quotationConfig.getUnambiguousQuotationMarkByType(QuotationDepth.Primary, QuotationDirection.Opening);
return this.quotationConfig.getUnambiguousQuotationMarkByType(
QuotationDepth.Primary,
PairedPunctuationDirection.Opening,
);
}

private getExpectedQuotationMarkFromAmbiguousCode(code: string): string | undefined {
Expand Down Expand Up @@ -159,9 +163,9 @@ class QuotationErrorFinder {
}

private addUnmatchedQuoteError(quotationMark: QuoteMetadata): void {
if (quotationMark.direction === QuotationDirection.Opening) {
if (quotationMark.direction === PairedPunctuationDirection.Opening) {
this.addUnmatchedOpeningQuoteError(quotationMark);
} else if (quotationMark.direction === QuotationDirection.Closing) {
} else if (quotationMark.direction === PairedPunctuationDirection.Closing) {
this.addUnmatchedClosingQuoteError(quotationMark);
}
}
Expand Down
27 changes: 15 additions & 12 deletions packages/punctuation-checker/src/quotation/quotation-config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AmbiguousPunctuationMap, PairedPunctuationConfig } from '../rule-set/rule-utils';
import { CharacterClassRegexBuilder, StringContextMatcher } from '../utils';
import { QuotationDepth, QuotationDirection } from './quotation-utils';
import { AmbiguousPunctuationMap, PairedPunctuationRule } from '../rule-set/rule-utils';
import { CharacterClassRegexBuilder, PairedPunctuationDirection, StringContextMatcher } from '../utils';
import { QuotationDepth } from './quotation-utils';

export class QuotationConfig {
private quotationLevels: PairedPunctuationConfig[] = [];
private quotationLevels: PairedPunctuationRule[] = [];
private ambiguousQuoteMap: AmbiguousPunctuationMap = new AmbiguousPunctuationMap();
private quotationsToIgnore: StringContextMatcher[] = [];
private nestingWarningDepth: QuotationDepth = QuotationDepth.fromNumber(4);
Expand Down Expand Up @@ -58,13 +58,13 @@ export class QuotationConfig {
return quoteRegexBuilder.makeGlobal().build();
}

public getPossibleQuoteDirections(quotationMark: string): QuotationDirection[] {
const directions: Set<QuotationDirection> = new Set<QuotationDirection>();
public getPossibleQuoteDirections(quotationMark: string): PairedPunctuationDirection[] {
const directions: Set<PairedPunctuationDirection> = new Set<PairedPunctuationDirection>();
if (this.openingQuoteRegex.test(quotationMark)) {
directions.add(QuotationDirection.Opening);
directions.add(PairedPunctuationDirection.Opening);
}
if (this.closingQuoteRegex.test(quotationMark)) {
directions.add(QuotationDirection.Closing);
directions.add(PairedPunctuationDirection.Closing);
}
if (this.ambiguousQuoteRegex.test(quotationMark)) {
for (const unambiguousMark of this.ambiguousQuoteMap.lookUpAmbiguousMark(quotationMark)) {
Expand Down Expand Up @@ -101,13 +101,16 @@ export class QuotationConfig {
return this.ambiguousQuoteRegex.test(quotationMark);
}

public getUnambiguousQuotationMarkByType(depth: QuotationDepth, direction: QuotationDirection): string | undefined {
public getUnambiguousQuotationMarkByType(
depth: QuotationDepth,
direction: PairedPunctuationDirection,
): string | undefined {
if (depth.asNumber() > this.quotationLevels.length) {
return undefined;
}

const unambiguousMark: string =
direction === QuotationDirection.Opening
direction === PairedPunctuationDirection.Opening
? this.quotationLevels[depth.asNumber() - 1].openingPunctuationMark
: this.quotationLevels[depth.asNumber() - 1].closingPunctuationMark;
return unambiguousMark;
Expand Down Expand Up @@ -142,7 +145,7 @@ export class QuotationConfig {
public static Builder = class {
readonly quotationConfig: QuotationConfig = new QuotationConfig();

public setTopLevelQuotationMarks(quotationPairConfig: PairedPunctuationConfig): this {
public setTopLevelQuotationMarks(quotationPairConfig: PairedPunctuationRule): this {
if (this.quotationConfig.quotationLevels.length > 0) {
this.quotationConfig.quotationLevels[0] = quotationPairConfig;
}
Expand All @@ -151,7 +154,7 @@ export class QuotationConfig {
return this;
}

public addNestedQuotationMarks(quotationPairConfig: PairedPunctuationConfig): this {
public addNestedQuotationMarks(quotationPairConfig: PairedPunctuationRule): this {
if (this.quotationConfig.quotationLevels.length === 0) {
throw new Error('You must set the top-level quotation marks before adding next quotation marks.');
}
Expand Down
Loading

0 comments on commit ef0d0dc

Please sign in to comment.