Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

intern

Because your real interns have better things to do than align your ppt boxes.

intern is a rule-based linter for PowerPoint (.pptx) files. Point it at a deck and it tells you exactly what’s wrong - misaligned boxes, inconsistent fonts, sloppy text, duplicate titles - and can automatically fix alignment, font-size, and whitespace problems.

Existing tools are proprietary Office add-ins or AI-powered web uploads. intern is the first open-source, rule-based CLI linter for PowerPoint: configurable, scriptable, and CI-friendly.

$ intern check quarterly.pptx

┌───────┬──────────────────────┬─────────┬────────────────┬──────┬───────────────────────────────────┬─────────────────────────────────────────┐
│ Slide ┆ Rule                 ┆ Type    ┆ Position       ┆ Id   ┆ Text                              ┆ Message                                 │
╞═══════╪══════════════════════╪═════════╪════════════════╪══════╪═══════════════════════════════════╪═════════════════════════════════════════╡
│ -     ┆ FONT_SIZE_VARIETY    ┆ -       ┆ -              ┆ -    ┆ -                                 ┆ 8 distinct body font sizes (limit: 3)   │
│       ┆                      ┆         ┆                ┆      ┆                                   ┆                                         │
│ 2     ┆ BULLET_LENGTH        ┆ Body    ┆ (40px, 132px)  ┆ 5    ┆ Our goals for the next quarter... ┆ bullet is 26 words (20-word limit)      │
│ 2     ┆ RIGHT_MARGIN         ┆ Body    ┆ (40px, 132px)  ┆ 5    ┆ Our goals for the next quarter... ┆ right edge at 905.4px (typical 927.0px) │
│       ┆                      ┆         ┆                ┆      ┆                                   ┆                                         │
│ 4     ┆ TITLE_TRAILING_PUNCT ┆ Title   ┆ (28px, 15px)   ┆ 12   ┆ Project Status.                   ┆ title ends with '.' - remove it         │
│       ┆                      ┆         ┆                ┆      ┆                                   ┆                                         │
│ 8     ┆ DUPLICATE_TITLE      ┆ Title   ┆ (28px, 15px)   ┆ 44   ┆ Overview                          ┆ same title as slide 6                   │
└───────┴──────────────────────┴─────────┴────────────────┴──────┴───────────────────────────────────┴─────────────────────────────────────────┘
5 violation(s) (5 error, 0 warning)

How it works

  1. intern unzips the .pptx and reads every slide’s shapes, text, and images.
  2. Each rule inspects the deck and reports violations.
  3. Most violations carry a suggested fix - alignment, font sizes, and whitespace - and intern fix applies them in place.

Exit code is 0 when the deck is clean or has only warnings, and 1 when an error-severity violation is found - so it drops straight into a CI pipeline.

Head to Installation to get started.

Installation

Pick whichever fits your setup - all three give you the same intern binary.

Homebrew - macOS & Linux

brew install markusz/intern/intern

Prebuilt binary

Download the archive for your platform from the latest release, extract it, and move intern onto your PATH.

PlatformArchive
macOS (Apple Silicon)intern-aarch64-apple-darwin.tar.gz
macOS (Intel)intern-x86_64-apple-darwin.tar.gz
Linux (x86-64)intern-x86_64-unknown-linux-gnu.tar.gz
Windows (x86-64)intern-x86_64-pc-windows-msvc.zip

One-liner for macOS/Linux (swap in the archive from the table):

curl -L https://github.com/markusz/intern/releases/latest/download/intern-aarch64-apple-darwin.tar.gz | tar xz
sudo mv intern /usr/local/bin/

Build from source

Requires Rust.

cargo install --path intern

Verify the install

intern --help

Command-line usage

intern has three subcommands: check reports violations, fix repairs the ones it can, and ignore writes a suppression directive into a slide’s speaker notes. check is the default action, so these two are equivalent:

intern deck.pptx
intern check deck.pptx

Every check and fix command accepts multiple files and directories - a directory is expanded to the .pptx files directly inside it:

intern check slides/ extra.pptx

No configuration is required to get started.

intern check

Reads each presentation and prints its violations. Exits 0 when every deck is clean or has only warnings, and 1 when an error-severity violation is found (see severity).

FlagDefaultDescription
--rules RULE_ID,...allRun only the specified rules
--disable RULE_ID,...noneSkip specific rules
--threshold <px>2Alignment tolerance in pixels
--slide <n>allAnalyze only slide n (1-based)
--output table|text|jsontableOutput format
--group-by slide|ruleslideGroup violations
--config <path>autoLoad settings from a specific file (Configuration)

An unknown rule id passed to --rules or --disable is rejected with an error rather than silently ignored.

intern fix

Applies the suggested fix for every fixable violation, writing each file in place. The original is backed up next to it as <file>.bak.

FlagDefaultDescription
--rules RULE_ID,...allRun only the specified rules
--disable RULE_ID,...noneSkip specific rules
--threshold <px>2Alignment tolerance in pixels
--slide <n>allFix only slide n (1-based)
--dry-runoffPrint what would change without writing

Not every rule is auto-fixable. Alignment, font-size, and whitespace rules carry a concrete fix; the remaining text-quality and structural rules report the problem but leave the change to you. See the rules reference.

intern ignore

Writes a suppression directive into a slide’s speaker notes without opening PowerPoint. Backs the original up to <file>.bak before writing.

intern ignore <file> -s <slide> -r <rule> [-e <element>]
FlagDescription
-s, --slide <n>Slide number (1-based, matches the Slide column in check output)
-r, --rule <ID>Rule ID to suppress (e.g. EMPTY_TEXTBOX)
-e, --element <id>Element id (Id column) - omit to suppress the rule for the whole slide

Suppress EMPTY_TEXTBOX for element 42 on slide 3:

intern ignore deck.pptx -s 3 -e 42 -r EMPTY_TEXTBOX
# appends: intern: disable(42) EMPTY_TEXTBOX

Suppress TITLE_Y for the whole of slide 1 (a title slide with a non-standard layout):

intern ignore deck.pptx -s 1 -r TITLE_Y
# appends: intern: disable TITLE_Y

The rule ID is validated; an unknown ID is rejected with an error. If the slide has no speaker-notes part, intern ignore creates one automatically.

Skipping checks

Skip an entire slide

To exclude a slide from every check - a title slide, a section divider, a deliberately different layout - add this line to its speaker notes:

intern: disable

To exclude it from only specific rules, list their ids:

intern: disable TITLE_Y, DUPLICATE_TITLE

Either way the slide is dropped before those rules run, so it affects neither the report nor their baselines (such as the median title position the other slides are compared against).

Skip a single element

The easiest way is intern ignore deck.pptx -s <slide> -r <rule> -e <element> - it writes the directive automatically. To add it by hand, put the element id (shown as Id in the table output) in parentheses:

intern: disable(42) EMPTY_TEXTBOX

Omit the rule list to suppress every rule for that element:

intern: disable(42)

Both syntaxes can appear on the same line as a slide-level directive:

intern: disable TITLE_Y
intern: disable(42) EMPTY_TEXTBOX

Multiple lines in the same slide’s speaker notes are all processed independently.

Use in CI

intern check exits 0 when every deck is clean and 1 when it finds an error-severity violation - wire that exit code straight into a pipeline. Point it at a directory to gate a whole folder of decks:

intern check slides/

JSON output is available for further processing:

intern check deck.pptx --output json > violations.json

Rules reference

intern ships 36 rules across four categories. Every rule has a stable id you can pass to --rules or --disable, or configure under [rules.<RULE_ID>] in .intern.toml.

Rules are either on (run by default) or off (must be opted in via enabled = true in [rules.<RULE_ID>] or named in --rules). The status is shown in the tables below.

Alignment

Geometric checks. All compare positions within a configurable pixel threshold.

Margin consistency (cross-slide)

Groups are treated as single units - only the group’s bounding box is considered, not its individual children.

RuleStatusWhat it catchesDefault threshold
LEFT_MARGINonSlide’s leftmost unit is off the typical left margin10 px
RIGHT_MARGINonSlide’s rightmost unit right edge is off the typical right margin10 px
BOTTOM_MARGINonContent extends deeper than the typical bottom margin (overflow only)10 px
TITLE_MARGINonGap between title and nearest content unit differs from the typical gap5 px

Proximity alignment (per-slide)

RuleStatusWhat it catchesDefault threshold
CLOSE_XonTwo units on the same slide have X positions within threshold - likely misaligned5 px
CLOSE_YonTwo units on the same slide have Y positions within threshold - likely misaligned5 px

Other alignment

RuleStatusWhat it catches
TITLE_YonTitle top edge inconsistent across slides
TITLE_X_WIDTHonTitle left edge or width inconsistent across slides
TEXT_ELEMENT_OVERLAPonTwo text-bearing elements on the same slide have overlapping rects
ELEMENT_OVERFLOWonElement extends outside the slide bounds

Typography

RuleStatusWhat it catchesLimit
TITLE_FONT_SIZEonTitle font size differs from the majority-
FONT_SIZE_VARIETYonToo many distinct body font sizes across the deck3 sizes
BODY_FONT_FAMILYonBody font family differs from the majority across slides-
BODY_TEXT_COLORoffBody text color differs from the majority across slides-
FONT_VARIETYonToo many distinct font families across the deck4 families
COLOR_VARIETYonToo many distinct text colors across the deck6 colors

BODY_TEXT_COLOR is off by default: color varies intentionally in most decks (branded slides, dark backgrounds, highlighted callouts).

Text quality

RuleStatusWhat it catchesLimit
DOUBLE_SPACEonParagraph contains two or more consecutive spaces-
LEADING_SPACEonParagraph starts with whitespace-
ALL_CAPSoffParagraph text is ALL CAPS-
REPEATED_WORDonTwo consecutive identical words (“the the”)-
BULLET_CAPITALIZATIONonBullets have inconsistent first-letter capitalization-
BULLET_PUNCTUATIONonBullet ending punctuation is inconsistent across the deck-
BULLET_LENGTHonBullet is too long20 words

ALL_CAPS is off by default: common in corporate decks for KPI labels, callout boxes, and section stamps.

Structure

RuleStatusWhat it catchesLimit
TITLE_PRESENToffSlide has no title element-
TITLE_LENGTHonTitle is too long10 words
TITLE_TRAILING_PUNCTonTitle ends with . , : or ;-
DUPLICATE_TITLEonTitle text is duplicated on another slide-
EMPTY_TEXTBOXonText box has no text content-
SLIDE_COUNToffDeck has too many slides20 slides

TITLE_PRESENT is off by default: section dividers and full-bleed image slides legitimately have no title element.

SLIDE_COUNT is off by default: the 20-slide limit is too deck-specific to be a useful default.

Auto-fixable rules

intern fix repairs the rules with an unambiguous correction - the alignment rules (snap to the peer median), the font-size rules, and DOUBLE_SPACE / LEADING_SPACE (normalise whitespace). The remaining text-quality and structural rules report the problem but leave the wording to you.

Threshold

Geometric comparisons use EMU (English Metric Units). The default threshold for most alignment rules is 2 px. The margin and proximity rules have their own defaults (10 px for LEFT_MARGIN, RIGHT_MARGIN, BOTTOM_MARGIN; 5 px for TITLE_MARGIN, CLOSE_X, CLOSE_Y). The global --threshold flag affects only rules that use the 2 px default; per-rule overrides always win:

intern check deck.pptx --threshold 5
# in .intern.toml - overrides the per-rule default for that rule only
[rules.LEFT_MARGIN]
threshold = 15

The word- and count-based limits (TITLE_LENGTH, BULLET_LENGTH, FONT_VARIETY, COLOR_VARIETY, SLIDE_COUNT) are tuned in each rule’s [rules.<RULE_ID>] table - see Configuration.

Configuration

Settings can live in a TOML file so you don’t have to repeat flags on every run. Every field is optional, and CLI flags always override whatever the file provides.

Where intern looks

intern loads the first file it finds, in this order:

  1. the path passed to --config <file>
  2. ./.intern.toml in the current directory (project config)
  3. $XDG_CONFIG_HOME/intern.toml, or ~/.config/intern.toml (user config)

If none exists, the built-in defaults apply. Files are not merged - the highest-precedence file wins as a whole, and CLI flags then layer on top of it.

A path passed to --config must exist, or intern exits with an error; the auto-discovered files are used only when present.

Example

threshold_px = 2                        # global alignment tolerance

disable = ["ALL_CAPS"]                  # turn rules off in bulk
# only  = ["TITLE_Y", "TITLE_X_WIDTH"]  # if set, ONLY these rules run

[output]
format = "table"      # table | text | json
group_by = "rule"     # slide | rule

[rules.TITLE_LENGTH]
max_words = 8

[rules.ALL_CAPS]
severity = "warning"  # report it, but don't fail CI

[rules.TITLE_Y]
threshold = 1         # tighter tolerance, just for this rule

[rules.SLIDE_COUNT]
enabled = true        # SLIDE_COUNT is off by default; enable it explicitly
max_slides = 40

Per-rule tables

Each rule can be configured in its own [rules.<RULE_ID>] table. A rule with no table runs with default settings - except SLIDE_COUNT, which is off by default (its slide limit is too deck-specific) and runs only when its table sets enabled = true.

  • enabled = false turns the rule off.

  • severity is "error" (the default) or "warning". intern check exits non-zero only when an error is found - warnings are reported but never fail CI. Demote noisy rules to "warning" to keep them advisory.

  • threshold overrides the global threshold_px for that rule (alignment rules only) - e.g. pixel-perfect titles alongside a looser grid.

  • The count-based rules take a limit:

    RuleKey
    TITLE_LENGTHmax_words
    BULLET_LENGTHmax_words
    FONT_VARIETYmax_families
    COLOR_VARIETYmax_colors
    SLIDE_COUNTmax_slides

Blunt controls

  • disable - a top-level list that turns rules off in bulk.
  • only - a top-level whitelist; when present, only the listed rules run.
  • Disabling always wins. If a rule is both whitelisted by only and disabled (via disable or enabled = false), it does not run and intern prints a warning.

Other settings

  • threshold_px - alignment tolerance in pixels for every geometric rule.
  • [output] - default format (table | text | json) and group_by (slide | rule) for intern check.

CLI flags override the file: --disable extends disable, and --rules replaces only.

Examples

Recipes for common situations - find your task in the quick reference, jump to the recipe, copy the command. New to the command line? Start with First run.

Quick reference

I want to…Command
Check one deckintern deck.pptx
Check every deck in a folderintern check slides/
Auto-fix what can be fixedintern fix deck.pptx
Preview fixes without savingintern fix deck.pptx --dry-run
Check a single slideintern check deck.pptx --slide 4
Turn a check offintern check deck.pptx --disable ALL_CAPS
Run only certain checksintern check deck.pptx --rules TITLE_Y
Be stricter or looser on alignmentintern check deck.pptx --threshold 1
Get machine-readable outputintern check deck.pptx --output json

First run

Never used a command-line tool? Three steps.

  1. Open a terminal. macOS: press Cmd+Space, type Terminal, Enter. Windows: open PowerShell from the Start menu.
  2. Go to your deck’s folder. If it is on your Desktop:
    cd Desktop
    
  3. Check it:
    intern quarterly.pptx
    

intern prints one row per problem:

┌───────┬──────────────────────┬─────────┬────────────────┬──────┬───────────────────────────────────┬─────────────────────────────────────────┐
│ Slide ┆ Rule                 ┆ Type    ┆ Position       ┆ Id   ┆ Text                              ┆ Message                                 │
╞═══════╪══════════════════════╪═════════╪════════════════╪══════╪═══════════════════════════════════╪═════════════════════════════════════════╡
│ 2     ┆ BULLET_LENGTH        ┆ Body    ┆ (40px, 132px)  ┆ 5    ┆ Our goals for the next quarter... ┆ bullet is 26 words (20-word limit)      │
│ 2     ┆ RIGHT_MARGIN         ┆ Body    ┆ (40px, 132px)  ┆ 5    ┆ Our goals for the next quarter... ┆ right edge at 905.4px (typical 927.0px) │
│       ┆                      ┆         ┆                ┆      ┆                                   ┆                                         │
│ 4     ┆ TITLE_TRAILING_PUNCT ┆ Title   ┆ (28px, 15px)   ┆ 12   ┆ Project Status.                   ┆ title ends with '.' - remove it         │
└───────┴──────────────────────┴─────────┴────────────────┴──────┴───────────────────────────────────┴─────────────────────────────────────────┘
3 violation(s) (3 error, 0 warning)
  • Slide - where the problem is (slide 1 is the first slide).
  • Rule - the check that fired; look it up in the Rules reference.
  • Type / Position / Id / Text - the offending element: its kind, top-left corner, shape id, and a short text excerpt. All - for a whole-slide problem.
  • Message - what is wrong, in plain words.

A clean deck prints No violations found.

Everyday tasks

Check a deck

intern deck.pptx

check is the default action, so the subcommand is optional. Pass a folder to check every .pptx inside it:

intern check slides/

Fix the easy problems

intern fix deck.pptx

Applies every fixable violation and saves a backup as deck.pptx.bak. Alignment, font-size, and whitespace issues are corrected; wording problems are reported for you to handle. The Rules reference marks which is which.

Preview before fixing

intern fix deck.pptx --dry-run

Lists what fix would change without touching the file.

Focus on one slide

intern check deck.pptx --slide 4

Slides count from 1. Deck-wide checks (like “all titles line up”) need every slide, so drop --slide for the full picture.

Skip a slide that is intentionally different

Section dividers, the title slide, a deliberately unique layout - exclude a slide by adding a line to its speaker notes:

intern: disable                        # skip every rule on this slide
intern: disable TITLE_Y, DUPLICATE_TITLE  # skip only these rules

The slide is dropped before those rules run, so it skews no baselines either.

Loosen or tighten alignment

intern check deck.pptx --threshold 5   # more forgiving
intern check deck.pptx --threshold 1   # pixel-perfect

Alignment tolerance in pixels (default 2). Anything off by less is treated as fine.

Turn rules on or off

intern check deck.pptx --disable ALL_CAPS,TITLE_TRAILING_PUNCT  # skip these
intern check deck.pptx --rules TITLE_Y,TITLE_X_WIDTH            # run only these

Rule ids come from the Rules reference; a typo is rejected with an error. To make settings permanent, use a config file (below).

Teams & CI

A shared team standard

Commit an .intern.toml to your project - everyone who runs intern there picks it up:

threshold_px = 3
disable = ["ALL_CAPS"]

[rules.TITLE_LENGTH]
max_words = 8

Full reference: Configuration.

Make a rule advisory instead of blocking

Every rule is an error by default and fails CI. To keep a rule’s findings visible without failing the build, set its severity to warning:

[rules.ALL_CAPS]
severity = "warning"

intern check exits non-zero only when an error-severity violation exists.

Your personal defaults

Settings you want on every deck you lint, regardless of project, go in ~/.config/intern.toml. A project’s .intern.toml wins over it when both exist.

Gate a CI build

intern check exits 0 when clean (or only warnings) and 1 on an error-severity violation - point it at a folder to gate every deck. GitHub Actions, saved as .github/workflows/decks.yml:

name: decks
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install intern
        run: |
          curl -L https://github.com/markusz/intern/releases/latest/download/intern-x86_64-unknown-linux-gnu.tar.gz | tar xz
          sudo mv intern /usr/local/bin/
      - run: intern check slides/

Feed results to other tools

intern check deck.pptx --output json

JSON output nests violations under each file:

{
  "files": [
    {
      "path": "deck.pptx",
      "violations": [
        {
          "rule_id": "TITLE_Y",
          "slide": 2,
          "element_type": "Title",
          "element_position": "(28px, 47px)",
          "element_id": 12,
          "excerpt": "Project Status",
          "message": "title 34.2px lower than most slides",
          "severity": "error"
        }
      ]
    }
  ]
}

Pipe it through jq - for example, list every slide with a title-alignment problem:

intern check deck.pptx --output json \
  | jq -r '.files[].violations[] | select(.rule_id == "TITLE_Y") | .slide'

Using the library

intern-core is the engine without the CLI - use it to build custom tooling, reporting pipelines, or editor integrations.

[dependencies]
intern-core = { git = "https://github.com/markusz/intern" }

Checking a presentation

#![allow(unused)]
fn main() {
use intern_core::{
    model::EMU_PER_PX,
    reader::read_presentation,
    rules::{all_rules, Limits, RuleContext},
};

let pres = read_presentation("deck.pptx")?;
let ctx = RuleContext {
    threshold: 2 * EMU_PER_PX,
    slide_width: pres.slide_width,
    slide_height: pres.slide_height,
};
let limits = Limits { slide_count: 30, ..Limits::default() };

let violations: Vec<_> = all_rules(&limits)
    .iter()
    .flat_map(|rule| rule.check(&pres.slides, &ctx))
    .collect();

for v in &violations {
    println!("{:?} - {}", v.rule_id, v.message);
}
}

Key types

  • reader::read_presentation - parses a .pptx into a Presentation (its slides plus the deck’s slide dimensions).
  • rules::all_rules - builds every rule, parameterised by Limits.
  • rules::Rule - the trait each rule implements: check(slides, ctx) takes a &[SlideData] and a &RuleContext and returns a Vec<Violation>.
  • rules::Violation - carries the rule id, slide, element, a structured ViolationMessage, and an optional Fix.
  • writer::apply_fixes - applies a slice of Fix values to a .pptx in place.

Geometry is measured in EMU (English Metric Units); EMU_PER_PX converts a pixel tolerance into the threshold the rules expect.