@faircopy/rules-default 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,90 @@
1
+ // src/no-complex-sentences.ts
2
+ var DEFAULT_OPTIONS = {
3
+ maxGradeLevel: 12,
4
+ minWords: 10
5
+ };
6
+ var noComplexSentences = {
7
+ id: "no-complex-sentences",
8
+ description: "Flag individual sentences whose Flesch-Kincaid grade level exceeds a target",
9
+ defaults: { ...DEFAULT_OPTIONS },
10
+ help: "Long, syllable-dense sentences are hard to read. Break them into shorter sentences that each make one point.",
11
+ check({ text, sourceMap, options }) {
12
+ const maxGradeLevel = options.maxGradeLevel ?? DEFAULT_OPTIONS.maxGradeLevel;
13
+ const minWords = options.minWords ?? DEFAULT_OPTIONS.minWords;
14
+ const diagnostics = [];
15
+ for (const { sentence, start, end } of getSentences(text)) {
16
+ const words = getWords(sentence);
17
+ if (words.length < minWords || words.length === 0) continue;
18
+ const syllables = words.reduce((sum, word) => sum + countSyllables(word), 0);
19
+ const grade = fleschKincaidGrade(words.length, 1, syllables);
20
+ if (grade <= maxGradeLevel) continue;
21
+ const sourceStart = sourceMap[start];
22
+ const sourceEnd = sourceMap[end - 1];
23
+ if (sourceStart === void 0 || sourceEnd === void 0) continue;
24
+ const roundedGrade = Math.round(grade * 10) / 10;
25
+ const suggest = {
26
+ description: "Split this sentence into shorter sentences, one idea each.",
27
+ edits: []
28
+ };
29
+ diagnostics.push({
30
+ ruleId: "no-complex-sentences",
31
+ severity: "warn",
32
+ message: `sentence readability is grade ${roundedGrade.toFixed(1)} \u2014 simplify to ${maxGradeLevel} or below`,
33
+ range: { start: sourceStart, end: sourceEnd + 1 },
34
+ help: noComplexSentences.help,
35
+ suggest
36
+ });
37
+ }
38
+ return diagnostics;
39
+ }
40
+ };
41
+ function getSentences(text) {
42
+ const sentences = [];
43
+ const abbreviationPattern = /\b(?:dr|mr|mrs|ms|prof|sr|jr|eg|ie|etc|vs|vol|fig|no)\.|\b(?:a|p)\.m\./gi;
44
+ const placeholder = "\0";
45
+ const masked = text.replace(abbreviationPattern, (match2, offset) => {
46
+ if (/\b(?:a|p)\.m\.$/i.test(match2)) {
47
+ const after = text.slice(offset + match2.length);
48
+ if (/^\s+(?:[A-Z]|$)/.test(after)) {
49
+ return match2[0] + placeholder + match2.slice(2);
50
+ }
51
+ }
52
+ return match2.replaceAll(".", placeholder);
53
+ });
54
+ const terminator = /[.!?]+/g;
55
+ let lastEnd = 0;
56
+ let match;
57
+ while ((match = terminator.exec(masked)) !== null) {
58
+ const end = match.index + match[0].length;
59
+ const sentence = masked.slice(lastEnd, end).replaceAll(placeholder, ".");
60
+ const trimmed = sentence.trimStart();
61
+ const leadingSpace = sentence.length - trimmed.length;
62
+ sentences.push({ sentence: trimmed, start: lastEnd + leadingSpace, end });
63
+ lastEnd = end;
64
+ }
65
+ return sentences;
66
+ }
67
+ function getWords(text) {
68
+ return text.toLowerCase().replace(/[^a-z0-9\s'-]/g, " ").split(/\s+/).filter((word) => word.length > 0 && /[a-z0-9]/.test(word));
69
+ }
70
+ function countSyllables(word) {
71
+ const cleaned = word.toLowerCase().replace(/[^a-z]/g, "");
72
+ if (!cleaned) return 0;
73
+ if (cleaned.length <= 3) return 1;
74
+ const vowels = cleaned.match(/[aeiouy]+/g);
75
+ if (!vowels) return 1;
76
+ let count = vowels.length;
77
+ if (cleaned.endsWith("e")) count--;
78
+ if (cleaned.endsWith("le") && cleaned.length > 2 && !/[aeiouy]$/.test(cleaned[cleaned.length - 3] ?? "")) {
79
+ count++;
80
+ }
81
+ return Math.max(1, count);
82
+ }
83
+ function fleschKincaidGrade(words, sentences, syllables) {
84
+ if (sentences === 0 || words === 0) return 0;
85
+ return 0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59;
86
+ }
87
+
1
88
  // src/no-em-dash.ts
2
89
  var noEmDash = {
3
90
  id: "no-em-dash",
@@ -110,14 +197,1167 @@ var noRhetoricalScaffolding = {
110
197
  }
111
198
  };
112
199
 
200
+ // src/no-non-inclusive-language.ts
201
+ var DEFAULT_TERMS = [
202
+ { term: "guys", alternatives: ["everyone", "team", "folks"] },
203
+ { term: "manpower", alternatives: ["workforce", "staffing", "personnel"] },
204
+ { term: "whitelist", alternatives: ["allowlist"] },
205
+ { term: "blacklist", alternatives: ["denylist", "blocklist"] },
206
+ { term: "master", alternatives: ["primary", "main", "leader"] },
207
+ { term: "slave", alternatives: ["secondary", "replica", "follower"] },
208
+ { term: "crazy", alternatives: ["unexpected", "intense", "extreme"] },
209
+ { term: "insane", alternatives: ["extreme", "unbelievable", "remarkable"] },
210
+ { term: "dumb", alternatives: ["unhelpful", "poor", "uninformed"] },
211
+ { term: "lame", alternatives: ["unimpressive", "inadequate", "weak"] },
212
+ { term: "sanity check", alternatives: ["quick check", "confidence check", "verification"] },
213
+ { term: "blind spot", alternatives: ["unaware area", "gap", "oversight"] },
214
+ { term: "grandfathered", alternatives: ["legacy status", "exempted"] },
215
+ { term: "mankind", alternatives: ["humanity", "humankind", "people"] }
216
+ ];
217
+ function escapeRegex(text) {
218
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
219
+ }
220
+ function buildPattern(term, exact) {
221
+ const escaped = escapeRegex(term);
222
+ const isPhrase = /\s/.test(term);
223
+ if (isPhrase) {
224
+ if (exact) {
225
+ return new RegExp(`\\b${escaped}\\b`, "gi");
226
+ }
227
+ return new RegExp(escaped, "gi");
228
+ }
229
+ return new RegExp(`\\b${escaped}\\b`, "gi");
230
+ }
231
+ var noNonInclusiveLanguage = {
232
+ id: "no-non-inclusive-language",
233
+ description: "Flag non-inclusive terms and suggest neutral alternatives",
234
+ defaults: { terms: DEFAULT_TERMS, allowedTerms: [] },
235
+ help: "Non-inclusive terms can alienate readers. Replace them with neutral alternatives that name the same idea without relying on identity, ability, or historical power metaphors.",
236
+ check({ text, sourceMap, options }) {
237
+ const diagnostics = [];
238
+ const terms = options.terms?.length ? options.terms : DEFAULT_TERMS;
239
+ const allowed = new Set((options.allowedTerms ?? []).map((term) => term.toLowerCase()));
240
+ for (const { term, alternatives, exact } of terms) {
241
+ if (allowed.has(term.toLowerCase())) continue;
242
+ const re = buildPattern(term, exact ?? false);
243
+ let m;
244
+ while ((m = re.exec(text)) !== null) {
245
+ const start = sourceMap[m.index];
246
+ const end = sourceMap[m.index + m[0].length - 1] + 1;
247
+ const suggestion = alternatives.join(", ");
248
+ diagnostics.push({
249
+ ruleId: "no-non-inclusive-language",
250
+ severity: "error",
251
+ message: `replace "${m[0]}" with a neutral alternative such as "${suggestion}"`,
252
+ range: { start, end },
253
+ help: noNonInclusiveLanguage.help
254
+ });
255
+ }
256
+ }
257
+ return diagnostics;
258
+ }
259
+ };
260
+
261
+ // src/no-redundant-phrases.ts
262
+ var DEFAULT_PHRASES = [
263
+ { phrase: "in order to", replacement: "to" },
264
+ { phrase: "due to the fact that", replacement: "because" },
265
+ { phrase: "in spite of the fact that", replacement: "although" },
266
+ { phrase: "at this point in time", replacement: "now" },
267
+ { phrase: "in the event that", replacement: "if" },
268
+ { phrase: "for the purpose of", replacement: "to" },
269
+ { phrase: "with regard to", replacement: "about" },
270
+ { phrase: "in close proximity to", replacement: "near" },
271
+ { phrase: "a large number of", replacement: "many" },
272
+ { phrase: "the reason is that", replacement: "because" },
273
+ { phrase: "in the vicinity of", replacement: "near" },
274
+ { phrase: "on the occasion of", replacement: "when" },
275
+ { phrase: "in view of the fact that", replacement: "because" },
276
+ { phrase: "owing to the fact that", replacement: "because" },
277
+ { phrase: "for the reason that", replacement: "because" },
278
+ { phrase: "in light of the fact that", replacement: "because" },
279
+ { phrase: "it is important to note that", replacement: "" },
280
+ { phrase: "it should be noted that", replacement: "" },
281
+ { phrase: "needless to say", replacement: "" },
282
+ { phrase: "it goes without saying that", replacement: "" }
283
+ ];
284
+ function escapeRegExp(value) {
285
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
+ }
287
+ function buildPhrasePattern(phrase) {
288
+ const escaped = escapeRegExp(phrase).replace(/\\s+/g, "\\s+");
289
+ return new RegExp(`\\b${escaped}\\b`, "gi");
290
+ }
291
+ var noRedundantPhrases = {
292
+ id: "no-redundant-phrases",
293
+ description: "Flag wordy redundant phrases and suggest concise replacements",
294
+ defaults: { phrases: DEFAULT_PHRASES },
295
+ help: "Redundant phrases pad copy with extra words that add no meaning. Replace them with the concise alternative, or delete the phrase entirely if the replacement is empty.",
296
+ check({ text, sourceMap, options }) {
297
+ const diagnostics = [];
298
+ const phrases = options.phrases?.length ? options.phrases : DEFAULT_PHRASES;
299
+ for (const { phrase, replacement } of phrases) {
300
+ const re = buildPhrasePattern(phrase);
301
+ let match;
302
+ while ((match = re.exec(text)) !== null) {
303
+ const matchedPhrase = match[0];
304
+ const start = sourceMap[match.index];
305
+ const end = sourceMap[match.index + matchedPhrase.length - 1] + 1;
306
+ const suggest = {
307
+ description: replacement ? `replace "${matchedPhrase}" with "${replacement}"` : `delete "${matchedPhrase}"`,
308
+ edits: [{ range: { start, end }, replacement }]
309
+ };
310
+ diagnostics.push({
311
+ ruleId: "no-redundant-phrases",
312
+ severity: "warn",
313
+ message: replacement ? `"${matchedPhrase}" is redundant \u2014 use "${replacement}"` : `"${matchedPhrase}" is redundant \u2014 delete it`,
314
+ range: { start, end },
315
+ help: noRedundantPhrases.help,
316
+ suggest
317
+ });
318
+ }
319
+ }
320
+ return diagnostics;
321
+ }
322
+ };
323
+
324
+ // src/no-passive-voice.ts
325
+ var DEFAULT_AUXILIARIES = ["is", "are", "was", "were", "be", "been", "being"];
326
+ var DEFAULT_PARTICIPLES = [
327
+ "accepted",
328
+ "accomplished",
329
+ "achieved",
330
+ "acquired",
331
+ "added",
332
+ "addressed",
333
+ "adjusted",
334
+ "admired",
335
+ "admitted",
336
+ "adopted",
337
+ "advanced",
338
+ "affected",
339
+ "afforded",
340
+ "agreed",
341
+ "allowed",
342
+ "announced",
343
+ "answered",
344
+ "anticipated",
345
+ "approved",
346
+ "arranged",
347
+ "asked",
348
+ "assembled",
349
+ "assessed",
350
+ "assigned",
351
+ "assisted",
352
+ "assumed",
353
+ "assured",
354
+ "attached",
355
+ "attacked",
356
+ "attempted",
357
+ "attended",
358
+ "attracted",
359
+ "avoided",
360
+ "awarded",
361
+ "based",
362
+ "beaten",
363
+ "become",
364
+ "begun",
365
+ "believed",
366
+ "belonged",
367
+ "benefited",
368
+ "betrayed",
369
+ "blamed",
370
+ "blessed",
371
+ "blocked",
372
+ "blown",
373
+ "boarded",
374
+ "boiled",
375
+ "booked",
376
+ "borrowed",
377
+ "bothered",
378
+ "bought",
379
+ "bound",
380
+ "branded",
381
+ "broken",
382
+ "brought",
383
+ "built",
384
+ "burned",
385
+ "burst",
386
+ "called",
387
+ "captured",
388
+ "carried",
389
+ "caused",
390
+ "caught",
391
+ "celebrated",
392
+ "challenged",
393
+ "changed",
394
+ "charged",
395
+ "chased",
396
+ "checked",
397
+ "chosen",
398
+ "claimed",
399
+ "cleaned",
400
+ "cleared",
401
+ "clicked",
402
+ "climbed",
403
+ "closed",
404
+ "coached",
405
+ "collected",
406
+ "combined",
407
+ "come",
408
+ "comforted",
409
+ "committed",
410
+ "communicated",
411
+ "compared",
412
+ "competed",
413
+ "completed",
414
+ "complicated",
415
+ "composed",
416
+ "computed",
417
+ "conceived",
418
+ "concentrated",
419
+ "concerned",
420
+ "concluded",
421
+ "conditioned",
422
+ "conducted",
423
+ "confirmed",
424
+ "connected",
425
+ "considered",
426
+ "consisted",
427
+ "constructed",
428
+ "consulted",
429
+ "consumed",
430
+ "contacted",
431
+ "contained",
432
+ "continued",
433
+ "contributed",
434
+ "controlled",
435
+ "converted",
436
+ "convinced",
437
+ "cooked",
438
+ "cost",
439
+ "counted",
440
+ "covered",
441
+ "created",
442
+ "crossed",
443
+ "crowded",
444
+ "crushed",
445
+ "cried",
446
+ "cut",
447
+ "damaged",
448
+ "danced",
449
+ "dated",
450
+ "dealt",
451
+ "decided",
452
+ "declared",
453
+ "declined",
454
+ "decorated",
455
+ "decreased",
456
+ "defeated",
457
+ "defended",
458
+ "defined",
459
+ "delayed",
460
+ "delivered",
461
+ "demanded",
462
+ "demonstrated",
463
+ "denied",
464
+ "departed",
465
+ "depended",
466
+ "described",
467
+ "deserved",
468
+ "designed",
469
+ "destroyed",
470
+ "detailed",
471
+ "detected",
472
+ "determined",
473
+ "developed",
474
+ "devoted",
475
+ "differed",
476
+ "digested",
477
+ "diminished",
478
+ "directed",
479
+ "discovered",
480
+ "discussed",
481
+ "displayed",
482
+ "distributed",
483
+ "disturbed",
484
+ "divided",
485
+ "done",
486
+ "doubled",
487
+ "doubted",
488
+ "drafted",
489
+ "dragged",
490
+ "drawn",
491
+ "dressed",
492
+ "driven",
493
+ "dropped",
494
+ "drowned",
495
+ "dug",
496
+ "earned",
497
+ "eaten",
498
+ "edited",
499
+ "educated",
500
+ "elected",
501
+ "eliminated",
502
+ "embarrassed",
503
+ "emerged",
504
+ "employed",
505
+ "enabled",
506
+ "encouraged",
507
+ "ended",
508
+ "engaged",
509
+ "engineered",
510
+ "enjoyed",
511
+ "entered",
512
+ "entertained",
513
+ "equipped",
514
+ "escaped",
515
+ "established",
516
+ "estimated",
517
+ "evaluated",
518
+ "evolved",
519
+ "examined",
520
+ "exceeded",
521
+ "exchanged",
522
+ "excited",
523
+ "excused",
524
+ "executed",
525
+ "exercised",
526
+ "exhausted",
527
+ "exhibited",
528
+ "expanded",
529
+ "expected",
530
+ "experienced",
531
+ "explained",
532
+ "exploded",
533
+ "explored",
534
+ "exported",
535
+ "exposed",
536
+ "expressed",
537
+ "extended",
538
+ "faced",
539
+ "failed",
540
+ "fallen",
541
+ "favored",
542
+ "feared",
543
+ "featured",
544
+ "fed",
545
+ "felt",
546
+ "fetched",
547
+ "fielded",
548
+ "filled",
549
+ "filmed",
550
+ "filtered",
551
+ "financed",
552
+ "finished",
553
+ "fired",
554
+ "fitted",
555
+ "fixed",
556
+ "flashed",
557
+ "flown",
558
+ "focused",
559
+ "folded",
560
+ "followed",
561
+ "forced",
562
+ "forgotten",
563
+ "formed",
564
+ "founded",
565
+ "framed",
566
+ "freed",
567
+ "frozen",
568
+ "frustrated",
569
+ "fueled",
570
+ "fulfilled",
571
+ "functioned",
572
+ "funded",
573
+ "gained",
574
+ "gathered",
575
+ "given",
576
+ "gone",
577
+ "governed",
578
+ "grabbed",
579
+ "graded",
580
+ "granted",
581
+ "greeted",
582
+ "grown",
583
+ "guaranteed",
584
+ "guarded",
585
+ "guessed",
586
+ "guided",
587
+ "handled",
588
+ "hanged",
589
+ "happened",
590
+ "harmed",
591
+ "harvested",
592
+ "hated",
593
+ "headed",
594
+ "healed",
595
+ "heard",
596
+ "heated",
597
+ "helped",
598
+ "hidden",
599
+ "highlighted",
600
+ "hired",
601
+ "hit",
602
+ "held",
603
+ "honored",
604
+ "hooked",
605
+ "hoped",
606
+ "hosted",
607
+ "hunted",
608
+ "hurried",
609
+ "hurt",
610
+ "identified",
611
+ "ignored",
612
+ "illustrated",
613
+ "imagined",
614
+ "implemented",
615
+ "implied",
616
+ "imported",
617
+ "imposed",
618
+ "impressed",
619
+ "improved",
620
+ "included",
621
+ "increased",
622
+ "indicated",
623
+ "influenced",
624
+ "informed",
625
+ "initiated",
626
+ "injured",
627
+ "inquired",
628
+ "inserted",
629
+ "inspected",
630
+ "inspired",
631
+ "installed",
632
+ "instructed",
633
+ "intended",
634
+ "interacted",
635
+ "interested",
636
+ "interrupted",
637
+ "interviewed",
638
+ "introduced",
639
+ "invented",
640
+ "invested",
641
+ "investigated",
642
+ "invited",
643
+ "involved",
644
+ "isolated",
645
+ "issued",
646
+ "joined",
647
+ "judged",
648
+ "jumped",
649
+ "justified",
650
+ "kept",
651
+ "kicked",
652
+ "killed",
653
+ "kissed",
654
+ "knocked",
655
+ "known",
656
+ "labeled",
657
+ "lacked",
658
+ "landed",
659
+ "lasted",
660
+ "launched",
661
+ "learned",
662
+ "leased",
663
+ "left",
664
+ "lent",
665
+ "let",
666
+ "licensed",
667
+ "lifted",
668
+ "lighted",
669
+ "liked",
670
+ "limited",
671
+ "linked",
672
+ "listed",
673
+ "listened",
674
+ "lived",
675
+ "loaded",
676
+ "located",
677
+ "locked",
678
+ "logged",
679
+ "looked",
680
+ "lost",
681
+ "loved",
682
+ "made",
683
+ "maintained",
684
+ "managed",
685
+ "manufactured",
686
+ "marked",
687
+ "marketed",
688
+ "married",
689
+ "mastered",
690
+ "matched",
691
+ "mattered",
692
+ "matured",
693
+ "meant",
694
+ "measured",
695
+ "met",
696
+ "mentioned",
697
+ "merged",
698
+ "messed",
699
+ "migrated",
700
+ "minded",
701
+ "missed",
702
+ "mixed",
703
+ "modified",
704
+ "monitored",
705
+ "moved",
706
+ "multiplied",
707
+ "named",
708
+ "narrowed",
709
+ "needed",
710
+ "negotiated",
711
+ "noted",
712
+ "noticed",
713
+ "obtained",
714
+ "occurred",
715
+ "offered",
716
+ "opened",
717
+ "operated",
718
+ "opposed",
719
+ "ordered",
720
+ "organized",
721
+ "oriented",
722
+ "originated",
723
+ "overcome",
724
+ "overlooked",
725
+ "owned",
726
+ "paced",
727
+ "packed",
728
+ "paid",
729
+ "painted",
730
+ "paired",
731
+ "parked",
732
+ "participated",
733
+ "passed",
734
+ "patented",
735
+ "paused",
736
+ "perceived",
737
+ "performed",
738
+ "permitted",
739
+ "persuaded",
740
+ "phased",
741
+ "picked",
742
+ "pictured",
743
+ "placed",
744
+ "planned",
745
+ "planted",
746
+ "played",
747
+ "pleased",
748
+ "plugged",
749
+ "pointed",
750
+ "polished",
751
+ "popped",
752
+ "possessed",
753
+ "posted",
754
+ "poured",
755
+ "powered",
756
+ "praised",
757
+ "prayed",
758
+ "preached",
759
+ "preceded",
760
+ "predicted",
761
+ "preferred",
762
+ "prepared",
763
+ "prescribed",
764
+ "presented",
765
+ "preserved",
766
+ "pressed",
767
+ "pretended",
768
+ "prevented",
769
+ "priced",
770
+ "printed",
771
+ "prioritized",
772
+ "processed",
773
+ "produced",
774
+ "profited",
775
+ "programmed",
776
+ "prohibited",
777
+ "promised",
778
+ "promoted",
779
+ "prompted",
780
+ "proposed",
781
+ "protected",
782
+ "proved",
783
+ "provided",
784
+ "published",
785
+ "pulled",
786
+ "pumped",
787
+ "punched",
788
+ "purchased",
789
+ "pursued",
790
+ "pushed",
791
+ "put",
792
+ "qualified",
793
+ "questioned",
794
+ "quit",
795
+ "quoted",
796
+ "raised",
797
+ "ranked",
798
+ "rated",
799
+ "reached",
800
+ "reacted",
801
+ "read",
802
+ "realized",
803
+ "received",
804
+ "recognized",
805
+ "recommended",
806
+ "reconciled",
807
+ "recorded",
808
+ "recovered",
809
+ "recruited",
810
+ "reduced",
811
+ "referred",
812
+ "reflected",
813
+ "refused",
814
+ "regarded",
815
+ "regulated",
816
+ "rejected",
817
+ "related",
818
+ "released",
819
+ "remained",
820
+ "remembered",
821
+ "reminded",
822
+ "removed",
823
+ "rendered",
824
+ "renewed",
825
+ "rented",
826
+ "repaired",
827
+ "repeated",
828
+ "replaced",
829
+ "replied",
830
+ "reported",
831
+ "represented",
832
+ "reproduced",
833
+ "requested",
834
+ "required",
835
+ "researched",
836
+ "reserved",
837
+ "resolved",
838
+ "respected",
839
+ "responded",
840
+ "restored",
841
+ "resulted",
842
+ "retained",
843
+ "retired",
844
+ "retrieved",
845
+ "returned",
846
+ "revealed",
847
+ "reviewed",
848
+ "revised",
849
+ "revived",
850
+ "rewarded",
851
+ "ridden",
852
+ "risen",
853
+ "rolled",
854
+ "rooted",
855
+ "rounded",
856
+ "ruled",
857
+ "run",
858
+ "rushed",
859
+ "sacrificed",
860
+ "said",
861
+ "sold",
862
+ "sampled",
863
+ "saved",
864
+ "scanned",
865
+ "scared",
866
+ "scheduled",
867
+ "scored",
868
+ "scraped",
869
+ "scratched",
870
+ "screened",
871
+ "searched",
872
+ "seasoned",
873
+ "seated",
874
+ "secured",
875
+ "seen",
876
+ "selected",
877
+ "sent",
878
+ "separated",
879
+ "served",
880
+ "serviced",
881
+ "set",
882
+ "settled",
883
+ "settled",
884
+ "shaped",
885
+ "shared",
886
+ "shocked",
887
+ "shaken",
888
+ "shaped",
889
+ "shipped",
890
+ "shocked",
891
+ "shot",
892
+ "shown",
893
+ "shut",
894
+ "signed",
895
+ "simplified",
896
+ "singled",
897
+ "sited",
898
+ "situated",
899
+ "sized",
900
+ "sketched",
901
+ "skilled",
902
+ "slammed",
903
+ "slashed",
904
+ "slid",
905
+ "slipped",
906
+ "slowed",
907
+ "smashed",
908
+ "smelled",
909
+ "smiled",
910
+ "smoked",
911
+ "snapped",
912
+ "soaked",
913
+ "sold",
914
+ "solved",
915
+ "sorted",
916
+ "sought",
917
+ "sounded",
918
+ "spared",
919
+ "sparked",
920
+ "spawned",
921
+ "spearheaded",
922
+ "specified",
923
+ "spent",
924
+ "spilled",
925
+ "spun",
926
+ "split",
927
+ "spoken",
928
+ "sponsored",
929
+ "spotted",
930
+ "spread",
931
+ "sprung",
932
+ "staged",
933
+ "stained",
934
+ "staked",
935
+ "stalled",
936
+ "stamped",
937
+ "started",
938
+ "stated",
939
+ "stationed",
940
+ "stayed",
941
+ "stolen",
942
+ "stepped",
943
+ "sticked",
944
+ "stimulated",
945
+ "stirred",
946
+ "stopped",
947
+ "stored",
948
+ "strained",
949
+ "streamed",
950
+ "strengthened",
951
+ "stressed",
952
+ "stretched",
953
+ "stricken",
954
+ "struck",
955
+ "structured",
956
+ "struggled",
957
+ "studied",
958
+ "stuffed",
959
+ "styled",
960
+ "submitted",
961
+ "substituted",
962
+ "succeeded",
963
+ "sucked",
964
+ "sued",
965
+ "suffered",
966
+ "suggested",
967
+ "suited",
968
+ "summed",
969
+ "supplied",
970
+ "supported",
971
+ "supposed",
972
+ "surprised",
973
+ "surrounded",
974
+ "surveyed",
975
+ "survived",
976
+ "suspected",
977
+ "suspended",
978
+ "sustained",
979
+ "swallowed",
980
+ "swapped",
981
+ "swept",
982
+ "swelled",
983
+ "swung",
984
+ "switched",
985
+ "tackled",
986
+ "tagged",
987
+ "taken",
988
+ "talked",
989
+ "tapped",
990
+ "targeted",
991
+ "tasted",
992
+ "taught",
993
+ "torn",
994
+ "tested",
995
+ "testified",
996
+ "texted",
997
+ "thanked",
998
+ "thrown",
999
+ "thrust",
1000
+ "ticked",
1001
+ "tied",
1002
+ "tightened",
1003
+ "timed",
1004
+ "tipped",
1005
+ "tired",
1006
+ "titled",
1007
+ "tolerated",
1008
+ "topped",
1009
+ "touched",
1010
+ "toured",
1011
+ "tracked",
1012
+ "traded",
1013
+ "trained",
1014
+ "transferred",
1015
+ "transformed",
1016
+ "translated",
1017
+ "transmitted",
1018
+ "transported",
1019
+ "trapped",
1020
+ "traveled",
1021
+ "treated",
1022
+ "trimmed",
1023
+ "tripled",
1024
+ "triumphed",
1025
+ "troubled",
1026
+ "trusted",
1027
+ "tried",
1028
+ "turned",
1029
+ "twisted",
1030
+ "typed",
1031
+ "undergone",
1032
+ "understood",
1033
+ "undertaken",
1034
+ "unfolded",
1035
+ "unified",
1036
+ "united",
1037
+ "updated",
1038
+ "upgraded",
1039
+ "upheld",
1040
+ "upset",
1041
+ "used",
1042
+ "utilized",
1043
+ "valued",
1044
+ "vanished",
1045
+ "varied",
1046
+ "verified",
1047
+ "vetoed",
1048
+ "viewed",
1049
+ "visited",
1050
+ "voiced",
1051
+ "voted",
1052
+ "waged",
1053
+ "waited",
1054
+ "walked",
1055
+ "wandered",
1056
+ "wanted",
1057
+ "warned",
1058
+ "warranted",
1059
+ "washed",
1060
+ "wasted",
1061
+ "watched",
1062
+ "weakened",
1063
+ "worn",
1064
+ "welcomed",
1065
+ "won",
1066
+ "wondered",
1067
+ "worked",
1068
+ "worried",
1069
+ "worshiped",
1070
+ "wounded",
1071
+ "written",
1072
+ "wrung",
1073
+ "yielded"
1074
+ ];
1075
+ function escapeRegExp2(value) {
1076
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1077
+ }
1078
+ function buildPassivePattern(auxiliaries, participles) {
1079
+ const auxPattern = auxiliaries.map(escapeRegExp2).join("|");
1080
+ const participlePattern = participles.map(escapeRegExp2).join("|");
1081
+ return new RegExp(`\\b(${auxPattern})\\s+(\\w+\\s+){0,3}(${participlePattern})\\b`, "gi");
1082
+ }
1083
+ var noPassiveVoice = {
1084
+ id: "no-passive-voice",
1085
+ description: "Flag likely passive-voice constructions using auxiliary + past participle patterns",
1086
+ defaults: {
1087
+ auxiliaries: DEFAULT_AUXILIARIES,
1088
+ participles: DEFAULT_PARTICIPLES,
1089
+ allowedPhrases: []
1090
+ },
1091
+ help: "Passive voice often hides the actor and adds drag. Prefer naming who did the action unless the actor genuinely does not matter.",
1092
+ check({ text, sourceMap, options }) {
1093
+ const diagnostics = [];
1094
+ const auxiliaries = options.auxiliaries?.length ? options.auxiliaries : DEFAULT_AUXILIARIES;
1095
+ const participles = options.participles?.length ? options.participles : DEFAULT_PARTICIPLES;
1096
+ const allowed = new Set((options.allowedPhrases ?? []).map((phrase) => phrase.toLowerCase()));
1097
+ const re = buildPassivePattern(auxiliaries, participles);
1098
+ let match;
1099
+ while ((match = re.exec(text)) !== null) {
1100
+ const matchedText = match[0];
1101
+ const lowerMatch = matchedText.toLowerCase();
1102
+ let allowedMatch = false;
1103
+ for (const phrase of allowed) {
1104
+ if (lowerMatch.includes(phrase.toLowerCase())) {
1105
+ allowedMatch = true;
1106
+ break;
1107
+ }
1108
+ }
1109
+ if (allowedMatch) continue;
1110
+ const start = sourceMap[match.index];
1111
+ const end = sourceMap[match.index + matchedText.length - 1] + 1;
1112
+ diagnostics.push({
1113
+ ruleId: "no-passive-voice",
1114
+ severity: "warn",
1115
+ message: `rewrite passive construction "${matchedText}" with a named actor`,
1116
+ range: { start, end },
1117
+ help: noPassiveVoice.help
1118
+ });
1119
+ }
1120
+ return diagnostics;
1121
+ }
1122
+ };
1123
+
1124
+ // src/no-cliches.ts
1125
+ var DEFAULT_PHRASES2 = [
1126
+ { phrase: "world-class", alternatives: ["top-tier", "exceptional", "outstanding"] },
1127
+ { phrase: "best-in-class", alternatives: ["leading", "top-performing", "category-leading"] },
1128
+ { phrase: "cutting-edge", alternatives: ["advanced", "modern", "latest"] },
1129
+ { phrase: "state-of-the-art", alternatives: ["advanced", "modern", "sophisticated"] },
1130
+ { phrase: "game changer", alternatives: ["breakthrough", "transformation", "major advance"] },
1131
+ { phrase: "game-changing", alternatives: ["transformative", "breakthrough", "revolutionary"] },
1132
+ { phrase: "think outside the box", alternatives: ["be creative", "innovate", "find a new approach"] },
1133
+ { phrase: "at the end of the day", alternatives: ["ultimately", "finally", "in summary"] },
1134
+ { phrase: "low-hanging fruit", alternatives: ["easy wins", "quick opportunities", "simple targets"] },
1135
+ { phrase: "move the needle", alternatives: ["make a measurable difference", "drive results", "create impact"] },
1136
+ { phrase: "circle back", alternatives: ["follow up", "reconnect", "return to this"] },
1137
+ { phrase: "give 110%", alternatives: ["do your best", "make a full effort", "go all in"] },
1138
+ { phrase: "hit the ground running", alternatives: ["start quickly", "get started immediately", "begin effectively"] },
1139
+ { phrase: "boil the ocean", alternatives: ["take on too much", "overcomplicate", "lose focus"] },
1140
+ { phrase: "paradigm shift", alternatives: ["fundamental change", "new approach", "transformation"] },
1141
+ { phrase: "next level", alternatives: ["advanced", "improved", "elevated"] },
1142
+ { phrase: "seamless", alternatives: ["smooth", "effortless", "frictionless"] },
1143
+ { phrase: "robust", alternatives: ["strong", "resilient", "reliable"] },
1144
+ { phrase: "leverage", alternatives: ["use", "take advantage of", "utilize"] },
1145
+ { phrase: "synergy", alternatives: ["collaboration", "combined effect", "partnership"] }
1146
+ ];
1147
+ function escapeRegex2(value) {
1148
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1149
+ }
1150
+ function buildPattern2(phrase) {
1151
+ const escaped = escapeRegex2(phrase);
1152
+ return new RegExp(`(?<!\\w)${escaped}(?!\\w)`, "gi");
1153
+ }
1154
+ var noCliches = {
1155
+ id: "no-cliches",
1156
+ description: "Flag overused or clich\xE9d phrases and suggest fresher alternatives",
1157
+ defaults: { phrases: DEFAULT_PHRASES2, allow: [] },
1158
+ help: "Clich\xE9s and overused phrases make copy feel generic and forgettable. Replace them with specific, concrete language that reflects your actual product or idea.",
1159
+ check({ text, sourceMap, options }) {
1160
+ const diagnostics = [];
1161
+ const phrases = options.phrases?.length ? options.phrases : DEFAULT_PHRASES2;
1162
+ const allowed = new Set((options.allow ?? []).map((phrase) => phrase.toLowerCase()));
1163
+ for (const { phrase, alternatives } of phrases) {
1164
+ if (allowed.has(phrase.toLowerCase())) continue;
1165
+ const re = buildPattern2(phrase);
1166
+ let match;
1167
+ while ((match = re.exec(text)) !== null) {
1168
+ const matchedPhrase = match[0];
1169
+ const start = sourceMap[match.index];
1170
+ const end = sourceMap[match.index + matchedPhrase.length - 1] + 1;
1171
+ const suggestion = alternatives.join(", ");
1172
+ diagnostics.push({
1173
+ ruleId: "no-cliches",
1174
+ severity: "warn",
1175
+ message: `replace "${matchedPhrase}" with a fresher alternative such as "${suggestion}"`,
1176
+ range: { start, end },
1177
+ help: noCliches.help
1178
+ });
1179
+ }
1180
+ }
1181
+ return diagnostics;
1182
+ }
1183
+ };
1184
+
1185
+ // src/no-repetitive-sentence-startings.ts
1186
+ var DEFAULT_OPTIONS2 = {
1187
+ threshold: 3,
1188
+ minWords: 3,
1189
+ allow: ["the", "a", "an", "it", "this", "that"]
1190
+ };
1191
+ function getSentences2(text) {
1192
+ const sentences = [];
1193
+ const abbreviationPattern = /\b(?:dr|mr|mrs|ms|prof|sr|jr|eg|ie|etc|vs|vol|fig|no)\.|\b(?:a|p)\.m\./gi;
1194
+ const placeholder = "\0";
1195
+ const masked = text.replace(abbreviationPattern, (match2, offset) => {
1196
+ if (/\b(?:a|p)\.m\.$/i.test(match2)) {
1197
+ const after = text.slice(offset + match2.length);
1198
+ if (/^\s+(?:[A-Z]|$)/.test(after)) {
1199
+ return match2[0] + placeholder + match2.slice(2);
1200
+ }
1201
+ }
1202
+ return match2.replaceAll(".", placeholder);
1203
+ });
1204
+ const terminator = /[.!?]+/g;
1205
+ let lastEnd = 0;
1206
+ let match;
1207
+ while ((match = terminator.exec(masked)) !== null) {
1208
+ const end = match.index + match[0].length;
1209
+ const sentence = masked.slice(lastEnd, end).replaceAll(placeholder, ".");
1210
+ const trimmed = sentence.trimStart();
1211
+ const leadingSpace = sentence.length - trimmed.length;
1212
+ sentences.push({ sentence: trimmed, start: lastEnd + leadingSpace, end });
1213
+ lastEnd = end;
1214
+ }
1215
+ const trailing = masked.slice(lastEnd).trim();
1216
+ if (trailing) {
1217
+ sentences.push({ sentence: trailing, start: lastEnd, end: text.length });
1218
+ }
1219
+ return sentences;
1220
+ }
1221
+ function getFirstWord(sentence) {
1222
+ const match = sentence.trim().match(/^[a-zA-Z0-9]+/);
1223
+ return match ? match[0].toLowerCase() : null;
1224
+ }
1225
+ function countWords(sentence) {
1226
+ return sentence.replace(/[^a-zA-Z0-9\s'-]/g, " ").split(/\s+/).filter((word) => word.length > 0 && /[a-zA-Z0-9]/.test(word)).length;
1227
+ }
1228
+ var noRepetitiveSentenceStartings = {
1229
+ id: "no-repetitive-sentence-startings",
1230
+ description: "Flag consecutive sentences that start with the same word",
1231
+ defaults: { ...DEFAULT_OPTIONS2 },
1232
+ help: "Starting several consecutive sentences with the same word creates a repetitive rhythm. Vary the sentence openings or combine related sentences to keep the reader engaged.",
1233
+ check({ text, sourceMap, options }) {
1234
+ const threshold = options.threshold ?? DEFAULT_OPTIONS2.threshold;
1235
+ const minWords = options.minWords ?? DEFAULT_OPTIONS2.minWords;
1236
+ const allowed = new Set((options.allow ?? DEFAULT_OPTIONS2.allow).map((word) => word.toLowerCase()));
1237
+ const diagnostics = [];
1238
+ const sentences = getSentences2(text);
1239
+ let runStart = 0;
1240
+ let runWord = null;
1241
+ let runLength = 0;
1242
+ for (let index = 0; index < sentences.length; index++) {
1243
+ const { sentence, start, end } = sentences[index];
1244
+ const firstWord = getFirstWord(sentence);
1245
+ const words = countWords(sentence);
1246
+ if (!firstWord || words < minWords || allowed.has(firstWord)) {
1247
+ if (runLength >= threshold && runWord) {
1248
+ const first = sentences[runStart];
1249
+ const last = sentences[index - 1];
1250
+ const sourceStart = sourceMap[first.start];
1251
+ const sourceEnd = sourceMap[last.end - 1];
1252
+ if (sourceStart !== void 0 && sourceEnd !== void 0) {
1253
+ diagnostics.push({
1254
+ ruleId: "no-repetitive-sentence-startings",
1255
+ severity: "warn",
1256
+ message: `${runLength} consecutive sentences start with "${runWord}" \u2014 vary the openings`,
1257
+ range: { start: sourceStart, end: sourceEnd + 1 },
1258
+ help: noRepetitiveSentenceStartings.help
1259
+ });
1260
+ }
1261
+ }
1262
+ runWord = null;
1263
+ runLength = 0;
1264
+ runStart = index + 1;
1265
+ continue;
1266
+ }
1267
+ if (firstWord === runWord) {
1268
+ runLength++;
1269
+ } else {
1270
+ if (runLength >= threshold && runWord) {
1271
+ const first = sentences[runStart];
1272
+ const last = sentences[index - 1];
1273
+ const sourceStart = sourceMap[first.start];
1274
+ const sourceEnd = sourceMap[last.end - 1];
1275
+ if (sourceStart !== void 0 && sourceEnd !== void 0) {
1276
+ diagnostics.push({
1277
+ ruleId: "no-repetitive-sentence-startings",
1278
+ severity: "warn",
1279
+ message: `${runLength} consecutive sentences start with "${runWord}" \u2014 vary the openings`,
1280
+ range: { start: sourceStart, end: sourceEnd + 1 },
1281
+ help: noRepetitiveSentenceStartings.help
1282
+ });
1283
+ }
1284
+ }
1285
+ runWord = firstWord;
1286
+ runStart = index;
1287
+ runLength = 1;
1288
+ }
1289
+ }
1290
+ if (runLength >= threshold && runWord) {
1291
+ const first = sentences[runStart];
1292
+ const last = sentences[sentences.length - 1];
1293
+ const sourceStart = sourceMap[first.start];
1294
+ const sourceEnd = sourceMap[last.end - 1];
1295
+ if (sourceStart !== void 0 && sourceEnd !== void 0) {
1296
+ diagnostics.push({
1297
+ ruleId: "no-repetitive-sentence-startings",
1298
+ severity: "warn",
1299
+ message: `${runLength} consecutive sentences start with "${runWord}" \u2014 vary the openings`,
1300
+ range: { start: sourceStart, end: sourceEnd + 1 },
1301
+ help: noRepetitiveSentenceStartings.help
1302
+ });
1303
+ }
1304
+ }
1305
+ return diagnostics;
1306
+ }
1307
+ };
1308
+
1309
+ // src/no-filler-words.ts
1310
+ var DEFAULT_WORDS2 = ["just"];
1311
+ var noFillerWords = {
1312
+ id: "no-filler-words",
1313
+ description: "Ban filler words that pad out a sentence without adding meaning",
1314
+ defaults: { words: DEFAULT_WORDS2 },
1315
+ help: 'Filler words like "just" dilute your claim. Remove the word; if the sentence then feels too blunt, rewrite the surrounding copy instead of softening it.',
1316
+ check({ text, sourceMap, options }) {
1317
+ const diagnostics = [];
1318
+ const words = options.words?.length ? options.words : DEFAULT_WORDS2;
1319
+ for (const word of words) {
1320
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1321
+ const re = new RegExp(`\\b${escaped}\\b`, "gi");
1322
+ let m;
1323
+ while ((m = re.exec(text)) !== null) {
1324
+ const start = sourceMap[m.index];
1325
+ const end = sourceMap[m.index + m[0].length - 1] + 1;
1326
+ diagnostics.push({
1327
+ ruleId: "no-filler-words",
1328
+ severity: "error",
1329
+ message: `remove "${m[0].toLowerCase()}" \u2014 it's filler`,
1330
+ range: { start, end },
1331
+ help: noFillerWords.help
1332
+ });
1333
+ }
1334
+ }
1335
+ return diagnostics;
1336
+ }
1337
+ };
1338
+
113
1339
  // src/index.ts
114
1340
  var ruleRegistry = /* @__PURE__ */ new Map([
1341
+ ["no-complex-sentences", noComplexSentences],
115
1342
  ["no-em-dash", noEmDash],
116
1343
  ["no-weasel-words", noWeaselWords],
117
- ["no-rhetorical-scaffolding", noRhetoricalScaffolding]
1344
+ ["no-rhetorical-scaffolding", noRhetoricalScaffolding],
1345
+ ["no-non-inclusive-language", noNonInclusiveLanguage],
1346
+ ["no-redundant-phrases", noRedundantPhrases],
1347
+ ["no-passive-voice", noPassiveVoice],
1348
+ ["no-cliches", noCliches],
1349
+ ["no-repetitive-sentence-startings", noRepetitiveSentenceStartings],
1350
+ ["no-filler-words", noFillerWords]
118
1351
  ]);
119
1352
  export {
1353
+ noCliches,
1354
+ noComplexSentences,
120
1355
  noEmDash,
1356
+ noFillerWords,
1357
+ noNonInclusiveLanguage,
1358
+ noPassiveVoice,
1359
+ noRedundantPhrases,
1360
+ noRepetitiveSentenceStartings,
121
1361
  noRhetoricalScaffolding,
122
1362
  noWeaselWords,
123
1363
  ruleRegistry