Prosody is a content analysis library that measures reading flow and cognitive load for mixed text and code content.
The library provides a three-stage processing pipeline:
- Parsing: Format-specific parsers convert content into interleaved content blocks
- Analysis: Block analyzers summarize each content block using configurable algorithms
- Summarization: Results are aggregated into final metrics including reading time
Prosody comes with:
Prosody.MDExParser: a markdown parser based on MDExProsody.TextParser: a plain text parserProsody.CodeAnalyzer: a code block analyzer that attempts to apply cognitive load adjustments that aren't captured by simple word countingProsody.TextAnalyzer: an implementation that emulates word processor counting algorithms based on configuration. There are three short-hand configurations::balanced: The default algorithm, which splits words in a way that matches human intution. Hyphenated words (fast-paced) and alternating words (and/or) are counted as separate words. Formatted numbers (1,234) are counted as single words. This is similar to what Apple Pages does.:minimal: This splits words on spaces, so thatfast-pacedandand/orare one word, butand / oris two words. This is most like Microsoft Word or LibreOffice Writer.:maximal: This splits words on space and punctuation, resulting in the highest word count.
The algorithm results are sometimes surprising, but are consistent:
Example :balanced:minimal:maximaltwo words2 2 2 and/or2 1 2 and / or2 2 2 fast-paced2 1 2 1,234.561 1 3 www.example.com1 1 3 bob@example.com1 1 3 A longer result on the sentence:
The CEO's Q3 buy/sell analysis shows revenue increased 23.8% year-over-year, reaching $4.2M through our e-commerce platform at shop.company.co.uk. Email investors@company.com for the full profit/loss report.
:balancedproduces 30 words:minimalproduces 25 words:maximalproduces 37 words
For details, see
Prosody.TextAnalyzer.
Example Usage
content = "# Hello World
This is some text.
IO.puts("Hello")
# Separated pipeline
with {:ok, blocks} <- Prosody.MDExParser.parse(content),
{:ok, results} <- Prosody.analyze_blocks(blocks),
{:ok, summary} <- Prosody.summarize(results) do
render(:analysis, content: content, summary: summary)
end
# Convenience wrapper
render(:analysis, content: content, summary: Prosody.analyze!(content, parser: :markdown))
Summary
Types
Analysis result from processing a content block.
A content block represents a segment of content with type and metadata.
Final summary of content analysis.
Functions
Produces summary/0 output with one function call, returning as {:ok, summary} or
{:error, reason}.
Produces summary/0 output with one function call, or raises an error.
Analyze content blocks using configured analyzers. Returns {:ok, result} or
{:error, reason}.
Analyze content blocks using configured analyzers (bang version). Returns the result or raises an error.
Parse content blocks from content using parser-specific parsers. Returns {:ok, blocks}
or {:error, reason}.
Parse content blocks from content using parser-specific parsers. Returns blocks or
raises an error.
Summarize analysis results into final metrics. Returns {:ok, summary} or
{:error, reason}.
Summarize analysis results into final metrics. Returns summary or raises an error.
Types
@type analysis() :: %{ optional(:lines) => nil | non_neg_integer(), words: non_neg_integer(), reading_words: non_neg_integer(), metadata: map() }
Analysis result from processing a content block.
words: Actual word countreading_words: Words adjusted for cognitive load (may differ fromwords)lines: Optional number of lines (relevant for code blocks)metadata: Analyzer-specific metadata
@type block() :: %{ type: :text | :code, content: String.t(), language: nil | String.t(), metadata: map() }
A content block represents a segment of content with type and metadata.
type: The content type (:textor:code)content: The actual content stringlanguage: Optional language hint (code block language, if available, or an ISO 3166-1 alpha-2 language code)metadata: Parser-specific metadata
@type summary() :: %{ words: non_neg_integer(), reading_time: nil | non_neg_integer(), code: nil | %{words: non_neg_integer(), lines: non_neg_integer()}, text: nil | %{words: non_neg_integer()}, metadata: map() }
Final summary of content analysis.
words: Total reading word count (may include cognitive load adjustments), this isreading_wordsfromanalysis/0.reading_time: Estimated reading time in minutescode: Code block metrics withwordsandlines(nil if no code blocks)text: Text block metrics withwords(nil if no text blocks)metadata: Summary-specific metadata
Functions
Produces summary/0 output with one function call, returning as {:ok, summary} or
{:error, reason}.
Options
parser(default::text): Parser configuration forparser/2.analyzers: Analyzer configuration foranalyze_blocks/2.words_per_minute: Reading speed forsummarize/2.min_reading_time: Minimum reading time forsummarize/2.
All options are passed to each pipeline step, except for the four noted above.
Examples
# Text parser
{:ok, summary} = Prosody.analyze(content)
# Explicit parser
{:ok, summary} = Prosody.analyze(content, parser: :markdown)
# Full configuration
{:ok, summary} = Prosody.analyze(content,
parser: {:markdown, strip_frontmatter: false},
analyzers: [MermaidAnalyzer, :default],
words_per_minute: 250
)
Produces summary/0 output with one function call, or raises an error.
Options
parser(default::text): Parser configuration forparser/2.analyzers: Analyzer configuration foranalyze_blocks/2.words_per_minute: Reading speed forsummarize/2.min_reading_time: Minimum reading time forsummarize/2.
All options are passed to each pipeline step, except for the four noted above.
Examples
# Text parser
summary = Prosody.analyze!(content)
# Explicit parser
summary = Prosody.analyze!(content, parser: :markdown)
# Full configuration
summary = Prosody.analyze!(content,
parser: {:markdown, strip_frontmatter: false},
analyzers: [MermaidAnalyzer, :default],
words_per_minute: 250
)
@spec analyze_blocks( block() | [block()], keyword() ) :: {:ok, [analysis()]} | {:error, String.t()}
Analyze content blocks using configured analyzers. Returns {:ok, result} or
{:error, reason}.
Options
analyzers: List of analyzers to run over the blocks
Analyze content blocks using configured analyzers (bang version). Returns the result or raises an error.
Options
analyzers: List of analyzers to run over the blocks
Parse content blocks from content using parser-specific parsers. Returns {:ok, blocks}
or {:error, reason}.
Options
parser(default:text): Content parser. Must be:markdown,:text,module/0, or{parser, opts}.- Other options are passed to the parser unless the parser is provided as
{parser, opts}
Examples
{:ok, blocks} = Prosody.parse(content, parser: :markdown)
{:ok, blocks} = Prosody.parse(content, parser: {:markdown, strip_frontmatter: false})
{:ok, blocks} = Prosody.parse(content, parser: {MyCustom.Parser, custom_opt: true})
Parse content blocks from content using parser-specific parsers. Returns blocks or
raises an error.
Options
parser(default:text): Content parser. Must be:markdown,:text,module/0, or{parser, opts}.- Other options are passed to the parser unless the parser is provided as
{parser, opts}
Examples
{:ok, blocks} = Prosody.parse(content, parser: :markdown)
{:ok, blocks} = Prosody.parse(content, parser: {:markdown, strip_frontmatter: false})
{:ok, blocks} = Prosody.parse(content, parser: {MyCustom.Parser, custom_opt: true})
Summarize analysis results into final metrics. Returns {:ok, summary} or
{:error, reason}.
Options
words_per_minute: Reading speed for time calculation (default: 200)min_reading_time: Minimum reading time in minutes (default: 1)
Examples
{:ok, summary} = Prosody.summarize(analysis, words_per_minute: 250)
Summarize analysis results into final metrics. Returns summary or raises an error.
Options
words_per_minute: Reading speed for time calculation (default: 200)min_reading_time: Minimum reading time in minutes (default: 1)
Examples
summary = Prosody.summarize!(analysis, words_per_minute: 250)