Repository: domluna/JuliaFormatter.jl
Pull request #269 (2020-08-01 04:53:52)

Format docstrings and markdown options
This PR does the following:

1. Hide formatting docstrings and markdown files behind options that default to `false`. The options are `format_markdown` (format .md files) and `format_docstrings` (format docstrings in .jl files)
2. Cleans up code from #255 
3. Adds `format_md` function which takes in a markdown source string as input and normalizes the markdown source and formats Julia code blocks.


Patch:
diff --git a/Project.toml b/Project.toml
index 452a97e6..2c86a027 100644
--- a/Project.toml
+++ b/Project.toml
@@ -4,8 +4,8 @@ authors = ["Dominique Luna <dluna132@gmail.com>"]
 version = "0.6.9"
 
 [deps]
-CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6"
 CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
+CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6"
 DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
 Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
 Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
@@ -13,8 +13,8 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 Tokenize = "0796e94c-ce3b-5d07-9a54-7f471281c624"
 
 [compat]
-CommonMark = "0.5.1"
 CSTParser = "1, 2"
+CommonMark = "0.5"
 DataStructures = "0.17"
 Documenter = "0.24"
 Tokenize = "^0.5.5"
diff --git a/src/JuliaFormatter.jl b/src/JuliaFormatter.jl
index cd7a4580..ddea0e73 100644
--- a/src/JuliaFormatter.jl
+++ b/src/JuliaFormatter.jl
@@ -43,46 +43,13 @@ include("pretty.jl")
 include("nest.jl")
 include("print.jl")
 
+include("markdown.jl")
+
 include("styles/yas.jl")
 
 # on Windows lines can end in "\r\n"
 normalize_line_ending(s::AbstractString) = replace(s, "\r\n" => "\n")
 
-function initial_processing(
-    text;
-    indent::Int = 4,
-    margin::Int = 92,
-    style::AbstractStyle = DefaultStyle(),
-    always_for_in::Bool = false,
-    whitespace_typedefs::Bool = false,
-    whitespace_ops_in_indices::Bool = false,
-    remove_extra_newlines::Bool = false,
-    import_to_using::Bool = false,
-    pipe_to_function_call::Bool = false,
-    short_to_long_function_def::Bool = false,
-    always_use_return::Bool = false,
-    whitespace_in_kwargs::Bool = true,
-    annotate_untyped_fields_with_any::Bool = true,
-)
-    x, ps = CSTParser.parse(CSTParser.ParseState(text), true)
-    ps.errored && error("Parsing error for input:\n\n$text")
-
-    opts = Options(
-        always_for_in = always_for_in,
-        whitespace_typedefs = whitespace_typedefs,
-        whitespace_ops_in_indices = whitespace_ops_in_indices,
-        remove_extra_newlines = remove_extra_newlines,
-        import_to_using = import_to_using,
-        pipe_to_function_call = pipe_to_function_call,
-        short_to_long_function_def = short_to_long_function_def,
-        always_use_return = always_use_return,
-        whitespace_in_kwargs = whitespace_in_kwargs,
-        annotate_untyped_fields_with_any = annotate_untyped_fields_with_any,
-    )
-    s = State(Document(text), indent, margin, opts)
-    style, x, s
-end
-
 """
     format_text(
         text::AbstractString;
@@ -99,6 +66,7 @@ end
         always_use_return::Bool = false,
         whitespace_in_kwargs::Bool = true,
         annotate_untyped_fields_with_any::Bool = true,
+        format_docstrings::Bool = false,
     )::String
 
 Formats a Julia source passed in as a string, returning the formatted
@@ -253,28 +221,61 @@ struct A
     arg1::Any
 end
 ```
+
+### `format_docstrings`
+
+Format code docstrings with the same options used for the code source.
+
+Markdown is formatted with [`CommonMark`](https://github.com/MichaelHatherly/CommonMark.jl) alongside Julia code.
 """
-function format_text(text::AbstractString; options...)
+function format_text(
+    text::AbstractString;
+    indent::Int = 4,
+    margin::Int = 92,
+    style::AbstractStyle = DefaultStyle(),
+    always_for_in::Bool = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
+    remove_extra_newlines::Bool = false,
+    import_to_using::Bool = false,
+    pipe_to_function_call::Bool = false,
+    short_to_long_function_def::Bool = false,
+    always_use_return::Bool = false,
+    whitespace_in_kwargs::Bool = true,
+    annotate_untyped_fields_with_any::Bool = true,
+    format_docstrings::Bool = false,
+)
     isempty(text) && return text
-    style, x, s = initial_processing(text; options...)
-    # no actual code
-    x.args[1].kind === Tokens.NOTHING && length(x) == 1 && return text
-    _format_text(style, s, x)
+    opts = Options(
+        indent_size = indent,
+        max_margin = margin,
+        always_for_in = always_for_in,
+        whitespace_typedefs = whitespace_typedefs,
+        whitespace_ops_in_indices = whitespace_ops_in_indices,
+        remove_extra_newlines = remove_extra_newlines,
+        import_to_using = import_to_using,
+        pipe_to_function_call = pipe_to_function_call,
+        short_to_long_function_def = short_to_long_function_def,
+        always_use_return = always_use_return,
+        whitespace_in_kwargs = whitespace_in_kwargs,
+        annotate_untyped_fields_with_any = annotate_untyped_fields_with_any,
+        format_docstrings = format_docstrings,
+    )
+    return format_text(text, style, opts)
 end
 
-function format_text(style, state, text)
-    x, ps = CSTParser.parse(CSTParser.ParseState(text), true)
+function format_text(text::AbstractString, style::AbstractStyle, opts::Options)
+    cst, ps = CSTParser.parse(CSTParser.ParseState(text), true)
     ps.errored && error("Parsing error for input:\n\n$text")
-    s = State(Document(text), state.indent, state.margin, state.opts)
-    _format_text(style, s, x)
+    cst.args[1].kind === Tokens.NOTHING && length(cst) == 1 && return text
+    return format_text(cst, style, State(Document(text), opts))
 end
 
-function _format_text(style, s, x)
-    opts = s.opts
-    t = pretty(style, x, s)
+function format_text(cst::CSTParser.EXPR, style::AbstractStyle, s::State)
+    t = pretty(style, cst, s)
     hascomment(s.doc, t.endline) && (add_node!(t, InlineComment(t.endline), s))
 
-    opts.pipe_to_function_call && pipe_to_function_call_pass!(t)
+    s.opts.pipe_to_function_call && pipe_to_function_call_pass!(t)
 
     flatten_fst!(t)
     nest!(style, t, s)
@@ -308,10 +309,13 @@ end
         filename::AbstractString;
         overwrite::Bool = true,
         verbose::Bool = false,
+        format_markdown::Bool = false,
         format_options...,
     )::Bool
 
-Formats the contents of `filename` assuming it's a Julia source file.
+Formats the contents of `filename` assuming it's a `.jl` or `.md` file. If it's a
+`.md` file, Julia code blocks will be formatted in addition to the markdown being
+normalized.
 
 ### File Options
 
@@ -322,6 +326,8 @@ be written to `foo_fmt.jl` instead.
 If `verbose` is `true` details related to formatting the file will be printed
 to `stdout`.
 
+If `format_markdown` is `.md` files are formatted.
+
 ### Formatting Options
 
 See [`format_text`](@ref) for description of formatting options.
@@ -335,30 +341,31 @@ function format_file(
     filename::AbstractString;
     overwrite::Bool = true,
     verbose::Bool = false,
+    format_markdown::Bool = false,
     format_options...,
 )::Bool
     path, ext = splitext(filename)
     shebang_pattern = r"^#!\s*/.*\bjulia[0-9.-]*\b"
-    if ext == ".md"
+    formatted_str = if ext == ".md"
+        format_markdown || return true
         verbose && println("Formatting $filename")
         str = String(read(filename))
-        style, parsed, state = initial_processing(""; format_options...)
-        formatted_str = format_docstring(style, state, str)[5:end-4]
-        formatted_str = replace(formatted_str, r"\n*$" => "\n")
-        overwrite ? write(filename, formatted_str) :
-        write(path * "_fmt" * ext, formatted_str)
-        return formatted_str == str
+        format_md(str; format_options...)
     elseif ext == ".jl" || match(shebang_pattern, readline(filename)) !== nothing
         verbose && println("Formatting $filename")
         str = String(read(filename))
-        formatted_str = format_text(str; format_options...)
-        formatted_str = replace(formatted_str, r"\n*$" => "\n")
-        overwrite ? write(filename, formatted_str) :
-        write(path * "_fmt" * ext, formatted_str)
-        return formatted_str == str
+        format_text(str; format_options...)
     else
         error("$filename must be a Julia (.jl) or Markdown (.md) source file")
     end
+    formatted_str = replace(formatted_str, r"\n*$" => "\n")
+
+    if overwrite
+        write(filename, formatted_str)
+    else
+        write(path * "_fmt" * ext, formatted_str)
+    end
+    return formatted_str == str
 end
 
 if VERSION < v"1.1.0"
diff --git a/src/markdown.jl b/src/markdown.jl
new file mode 100644
index 00000000..b318a5dc
--- /dev/null
+++ b/src/markdown.jl
@@ -0,0 +1,70 @@
+"""
+    format_md(
+        text::AbstractString;
+        indent::Int = 4,
+        margin::Int = 92,
+        style::AbstractStyle = DefaultStyle(),
+        always_for_in::Bool = false,
+        whitespace_typedefs::Bool = false,
+        whitespace_ops_in_indices::Bool = false,
+        remove_extra_newlines::Bool = false,
+        import_to_using::Bool = false,
+        pipe_to_function_call::Bool = false,
+        short_to_long_function_def::Bool = false,
+        always_use_return::Bool = false,
+        whitespace_in_kwargs::Bool = true,
+        annotate_untyped_fields_with_any::Bool = true,
+        format_docstrings::Bool = false,
+    )::String
+
+Normalizes the Markdown source and formats Julia code blocks.
+
+See [`format_text`](@ref) for description of formatting options.
+"""
+function format_md(
+    text::AbstractString;
+    indent::Int = 4,
+    margin::Int = 92,
+    style::AbstractStyle = DefaultStyle(),
+    always_for_in::Bool = false,
+    whitespace_typedefs::Bool = false,
+    whitespace_ops_in_indices::Bool = false,
+    remove_extra_newlines::Bool = false,
+    import_to_using::Bool = false,
+    pipe_to_function_call::Bool = false,
+    short_to_long_function_def::Bool = false,
+    always_use_return::Bool = false,
+    whitespace_in_kwargs::Bool = true,
+    annotate_untyped_fields_with_any::Bool = true,
+    format_docstrings::Bool = false,
+)
+    isempty(text) && return text
+    opts = Options(
+        indent_size = indent,
+        max_margin = margin,
+        always_for_in = always_for_in,
+        whitespace_typedefs = whitespace_typedefs,
+        whitespace_ops_in_indices = whitespace_ops_in_indices,
+        remove_extra_newlines = remove_extra_newlines,
+        import_to_using = import_to_using,
+        pipe_to_function_call = pipe_to_function_call,
+        short_to_long_function_def = short_to_long_function_def,
+        always_use_return = always_use_return,
+        whitespace_in_kwargs = whitespace_in_kwargs,
+        annotate_untyped_fields_with_any = annotate_untyped_fields_with_any,
+        format_docstrings = format_docstrings,
+    )
+    formatted = markdown(enable!(
+        Parser(),
+        [
+            AdmonitionRule(),
+            FootnoteRule(),
+            MathRule(),
+            TableRule(),
+            FormatRule(style, opts),
+        ],
+    )(
+        text,
+    ))
+    return formatted
+end
diff --git a/src/nest.jl b/src/nest.jl
index e798debe..9aec8907 100644
--- a/src/nest.jl
+++ b/src/nest.jl
@@ -91,7 +91,7 @@ function dedent!(fst::FST, s::State)
     if is_leaf(fst)
         s.line_offset += length(fst)
         if is_closer(fst) || fst.typ === NOTCODE
-            fst.indent -= s.indent_size
+            fst.indent -= s.opts.indent_size
         end
         return
     end
@@ -99,14 +99,14 @@ function dedent!(fst::FST, s::State)
     fst.typ === CSTParser.StringH && return
 
     # dedent
-    fst.indent -= s.indent_size
+    fst.indent -= s.opts.indent_size
 
     fst.force_nest && return
 
     nl_inds = findall(n -> n.typ === NEWLINE && !n.force_nest, fst.nodes)
     length(nl_inds) > 0 || return
     margin = s.line_offset + fst.extra_margin + length(fst)
-    margin <= s.margin || return
+    margin <= s.opts.max_margin || return
     unnest!(fst, nl_inds)
 end
 
@@ -140,7 +140,7 @@ function nest_if_over_margin!(
         margin += sum(length.(fst[idx:stop_idx-1]))
     end
 
-    if margin > s.margin || is_comment(fst[idx+1]) || is_comment(fst[idx-1])
+    if margin > s.opts.max_margin || is_comment(fst[idx+1]) || is_comment(fst[idx-1])
         fst[idx] = Newline(length = fst[idx].len)
         s.line_offset = fst.indent
     else
@@ -198,7 +198,7 @@ function nest!(ds::DefaultStyle, fst::FST, s::State)
     elseif fst.typ === CSTParser.BinaryOpCall
         line_margin = s.line_offset + length(fst) + fst.extra_margin
         if s.opts.short_to_long_function_def &&
-           line_margin > s.margin &&
+           line_margin > s.opts.max_margin &&
            fst.ref !== nothing &&
            CSTParser.defines_function(fst.ref[])
 
@@ -306,11 +306,11 @@ function n_using!(ds::DefaultStyle, fst::FST, s::State)
     style = getstyle(ds)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
     idx = findfirst(n -> n.typ === PLACEHOLDER, fst.nodes)
-    if idx !== nothing && (line_margin > s.margin || fst.force_nest)
-        fst.indent += s.indent_size
+    if idx !== nothing && (line_margin > s.opts.max_margin || fst.force_nest)
+        fst.indent += s.opts.indent_size
 
         if !fst.force_nest
-            if fst.indent + sum(length.(fst[idx+1:end])) <= s.margin
+            if fst.indent + sum(length.(fst[idx+1:end])) <= s.opts.max_margin
                 fst[idx] = Newline(length = fst[idx].len)
                 walk(reset_line_offset!, fst, s)
                 return
@@ -350,13 +350,13 @@ function n_tupleh!(ds::DefaultStyle, fst::FST, s::State)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
     idx = findlast(n -> n.typ === PLACEHOLDER, fst.nodes)
     opener = length(fst.nodes) > 0 ? is_opener(fst[1]) : false
-    if idx !== nothing && (line_margin > s.margin || fst.force_nest)
+    if idx !== nothing && (line_margin > s.opts.max_margin || fst.force_nest)
         line_offset = s.line_offset
         if opener
             fst[end].indent = fst.indent
         end
         if fst.typ !== CSTParser.TupleH || opener
-            fst.indent += s.indent_size
+            fst.indent += s.opts.indent_size
         end
 
         for (i, n) in enumerate(fst.nodes)
@@ -427,7 +427,7 @@ function n_comprehension!(ds::DefaultStyle, fst::FST, s::State; indent = -1)
     style = getstyle(ds)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
     closer = is_closer(fst[end])
-    if closer && (line_margin > s.margin || fst.force_nest)
+    if closer && (line_margin > s.opts.max_margin || fst.force_nest)
         idx = findfirst(n -> n.typ === PLACEHOLDER, fst.nodes)
         if idx !== nothing
             fst[idx] = Newline(length = fst[idx].len)
@@ -437,8 +437,8 @@ function n_comprehension!(ds::DefaultStyle, fst::FST, s::State; indent = -1)
             fst[idx] = Newline(length = fst[idx].len)
         end
 
-        add_indent!(fst, s, s.indent_size)
-        fst[end].indent = fst.indent - s.indent_size
+        add_indent!(fst, s, s.opts.indent_size)
+        fst[end].indent = fst.indent - s.opts.indent_size
     end
 
     for (i, n) in enumerate(fst.nodes)
@@ -492,10 +492,10 @@ function n_call!(ds::DefaultStyle, fst::FST, s::State)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
     idx = findlast(n -> n.typ === PLACEHOLDER, fst.nodes)
 
-    if idx !== nothing && (line_margin > s.margin || fst.force_nest)
+    if idx !== nothing && (line_margin > s.opts.max_margin || fst.force_nest)
         line_offset = s.line_offset
         fst[end].indent = fst.indent
-        fst.indent += s.indent_size
+        fst.indent += s.opts.indent_size
 
         for (i, n) in enumerate(fst.nodes)
             if n.typ === NEWLINE
@@ -551,7 +551,7 @@ n_typedvcat!(style::S, fst::FST, s::State) where {S<:AbstractStyle} =
 function n_whereopcall!(ds::DefaultStyle, fst::FST, s::State)
     style = getstyle(ds)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
-    if line_margin > s.margin || fst.force_nest
+    if line_margin > s.opts.max_margin || fst.force_nest
         line_offset = s.line_offset
         Blen = sum(length.(fst[2:end]))
 
@@ -564,8 +564,9 @@ function n_whereopcall!(ds::DefaultStyle, fst::FST, s::State)
             fst[end].indent = fst.indent
         end
 
-        over = (s.line_offset + Blen + fst.extra_margin > s.margin) || fst.force_nest
-        fst.indent += s.indent_size
+        over =
+            (s.line_offset + Blen + fst.extra_margin > s.opts.max_margin) || fst.force_nest
+        fst.indent += s.opts.indent_size
         for (i, n) in enumerate(fst[2:end])
             if n.typ === NEWLINE
                 s.line_offset = fst.indent
@@ -603,7 +604,7 @@ n_whereopcall!(style::S, fst::FST, s::State) where {S<:AbstractStyle} =
 function n_conditionalopcall!(ds::DefaultStyle, fst::FST, s::State)
     style = getstyle(ds)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
-    if line_margin > s.margin || fst.force_nest
+    if line_margin > s.opts.max_margin || fst.force_nest
         line_offset = s.line_offset
         fst.indent = s.line_offset
         phs = reverse(findall(n -> n.typ === PLACEHOLDER, fst.nodes))
@@ -615,7 +616,7 @@ function n_conditionalopcall!(ds::DefaultStyle, fst::FST, s::State)
                 l1 = sum(length.(fst[1:idx-1]))
                 l2 = sum(length.(fst[idx:nidx-1]))
                 width = line_offset + l1 + l2
-                if fst.force_nest || width > s.margin
+                if fst.force_nest || width > s.opts.max_margin
                     fst[idx] = Newline(length = fst[idx].len)
                 end
             end
@@ -645,8 +646,8 @@ function n_conditionalopcall!(ds::DefaultStyle, fst::FST, s::State)
                 else
                     width += sum(length.(fst[i+1:i+3]))
                 end
-                # @debug "" s.line_offset l  s.margin
-                if width <= s.margin
+                # @debug "" s.line_offset l  s.opts.max_margin
+                if width <= s.opts.max_margin
                     fst[i] = Whitespace(1)
                 else
                     s.line_offset = fst.indent
@@ -673,7 +674,8 @@ function n_binaryopcall!(ds::DefaultStyle, fst::FST, s::State)
     idxs = findall(n -> n.typ === PLACEHOLDER, fst.nodes)
     rhs = fst[end]
     rhs.typ === CSTParser.Block && (rhs = rhs[1])
-    if length(idxs) == 2 && (line_margin > s.margin || fst.force_nest || rhs.force_nest)
+    if length(idxs) == 2 &&
+       (line_margin > s.opts.max_margin || fst.force_nest || rhs.force_nest)
         line_offset = s.line_offset
         i1 = idxs[1]
         i2 = idxs[2]
@@ -688,9 +690,9 @@ function n_binaryopcall!(ds::DefaultStyle, fst::FST, s::State)
             is_standalone_shortcircuit(cst)
 
         if indent_nest
-            s.line_offset = fst.indent + s.indent_size
-            fst[i2] = Whitespace(s.indent_size)
-            add_indent!(fst[end], s, s.indent_size)
+            s.line_offset = fst.indent + s.opts.indent_size
+            fst[i2] = Whitespace(s.opts.indent_size)
+            add_indent!(fst[end], s, s.opts.indent_size)
         else
             fst.indent = s.line_offset
         end
@@ -734,7 +736,7 @@ function n_binaryopcall!(ds::DefaultStyle, fst::FST, s::State)
                 line_margin += rw
             end
 
-            if line_margin + fst.extra_margin <= s.margin
+            if line_margin + fst.extra_margin <= s.opts.max_margin
                 fst[i1] = Whitespace(1)
                 if indent_nest
                     fst[i2] = Whitespace(0)
@@ -798,12 +800,12 @@ function n_block!(ds::DefaultStyle, fst::FST, s::State; indent = -1)
     style = getstyle(ds)
     line_margin = s.line_offset + length(fst) + fst.extra_margin
     idx = findfirst(n -> n.typ === PLACEHOLDER, fst.nodes)
-    if idx !== nothing && (line_margin > s.margin || fst.force_nest)
+    if idx !== nothing && (line_margin > s.opts.max_margin || fst.force_nest)
         line_offset = s.line_offset
         indent >= 0 && (fst.indent = indent)
 
         if fst.typ === CSTParser.ChainOpCall && is_standalone_shortcircuit(fst.ref[])
-            fst.indent += s.indent_size
+            fst.indent += s.opts.indent_size
         end
 
         for (i, n) in enumerate(fst.nodes)
diff --git a/src/options.jl b/src/options.jl
index 3740d73f..42686996 100644
--- a/src/options.jl
+++ b/src/options.jl
@@ -1,4 +1,6 @@
 Base.@kwdef struct Options
+    indent_size::Int = 4
+    max_margin::Int = 92
     always_for_in::Bool = false
     whitespace_typedefs::Bool = false
     whitespace_ops_in_indices::Bool = false
@@ -9,4 +11,5 @@ Base.@kwdef struct Options
     always_use_return::Bool = false
     whitespace_in_kwargs::Bool = true
     annotate_untyped_fields_with_any::Bool = true
+    format_docstrings::Bool = false
 end
diff --git a/src/passes.jl b/src/passes.jl
index 5c73f5a1..10bb92f1 100644
--- a/src/passes.jl
+++ b/src/passes.jl
@@ -222,8 +222,8 @@ function short_to_long_function_def!(fst::FST, s::State)
 
         # body
         s.opts.always_use_return && prepend_return!(fst[end], s)
-        add_node!(funcdef, fst[end], s, max_padding = s.indent_size)
-        add_indent!(funcdef[end], s, s.indent_size)
+        add_node!(funcdef, fst[end], s, max_padding = s.opts.indent_size)
+        add_indent!(funcdef[end], s, s.opts.indent_size)
 
         # end
         kw = FST(CSTParser.KEYWORD, fst[end].startline, fst[end].endline, "end")
@@ -251,8 +251,8 @@ function short_to_long_function_def!(fst::FST, s::State)
 
         # body
         s.opts.always_use_return && prepend_return!(fst[end], s)
-        add_node!(funcdef, fst[end], s, max_padding = s.indent_size)
-        add_indent!(funcdef[end], s, s.indent_size)
+        add_node!(funcdef, fst[end], s, max_padding = s.opts.indent_size)
+        add_indent!(funcdef[end], s, s.opts.indent_size)
 
         # end
         kw = FST(CSTParser.KEYWORD, fst[end].startline, fst[end].endline, "end")
diff --git a/src/pretty.jl b/src/pretty.jl
index 7cdc4f4a..c6c3edd9 100644
--- a/src/pretty.jl
+++ b/src/pretty.jl
@@ -229,76 +229,59 @@ end
 p_punctuation(style::S, cst::CSTParser.EXPR, s::State) where {S<:AbstractStyle} =
     p_punctuation(DefaultStyle(style), cst, s)
 
-struct FormatRule{Style,State}
-    style::Style
-    state::State
+struct FormatRule{T<:AbstractStyle}
+    style::T
+    opts::Options
 end
+format_text(text::AbstractString, fr::FormatRule) = format_text(text, fr.style, fr.opts)
 
 block_modifier(rule::FormatRule) =
     Rule(1) do parser, block
-        if block.t isa CodeBlock
-            style = rule.style
-            state = rule.state
-            language = block.t.info
-            code = block.literal
-            if startswith(language, "@example") ||
-               startswith(language, "@repl") ||
-               startswith(language, "@eval")
-                block.literal = format_text(code)
-            elseif startswith(language, "jldoctest")
-                # TODO: figure out why an extra indent is necessary
-                indented_state = State(
-                    state.doc,
-                    state.indent_size,
-                    state.indent + state.indent_size,
-                    state.offset,
-                    state.line_offset,
-                    state.margin,
-                    state.on,
-                    state.opts,
-                )
-                block.literal = if occursin(r"^julia> "m, code)
-                    doctests = IOBuffer()
-                    first_test = true
-                    for (an_input, output) in repl_splitter(code)
-                        if first_test
-                            first_test = false
+        block.t isa CodeBlock || return
+        language = block.t.info
+        code = block.literal
+        if startswith(language, "@example") ||
+           startswith(language, "@repl") ||
+           startswith(language, "@eval") ||
+           startswith(language, "julia")
+            block.literal = format_text(code, rule)
+        elseif startswith(language, "jldoctest")
+            block.literal = if occursin(r"^julia> "m, code)
+                doctests = IOBuffer()
+                first_test = true
+                for (an_input, output) in repl_splitter(code)
+                    if first_test
+                        first_test = false
+                    else
+                        write(doctests, "\n\n")
+                    end
+                    write(doctests, "julia> ")
+                    first_line = true
+                    for line in split(format_text(an_input, rule), '\n')
+                        if first_line
+                            first_line = false
                         else
-                            write(doctests, "\n\n")
-                        end
-                        write(doctests, "julia> ")
-                        first_line = true
-                        for line in
-                            split(format_text(style, indented_state, an_input), '\n')
-                            if first_line
-                                first_line = false
-                            else
-                                write(doctests, "\n       ")
-                            end
-                            write(doctests, line)
+                            write(doctests, "\n       ")
                         end
-                        write(doctests, '\n')
-                        write(doctests, output)
+                        write(doctests, line)
                     end
                     write(doctests, '\n')
-                    String(take!(doctests))
-                else
-                    an_input, output = split(code, r"\n+# output\n+", limit = 2)
-                    string(
-                        format_text(
-                            style,
-                            indented_state,
-                            format_text(style, state, String(an_input)),
-                        ),
-                        "\n\n# output\n\n",
-                        output,
-                    )
+                    write(doctests, output)
                 end
+                write(doctests, '\n')
+                String(take!(doctests))
+            else
+                an_input, output = split(code, r"\n+# output\n+", limit = 2)
+                string(
+                    format_text(format_text(String(an_input), rule), rule),
+                    "\n\n# output\n\n",
+                    output,
+                )
             end
         end
     end
 
-function format_docstring(style, state, text)
+function format_docstring(style::AbstractStyle, state::State, text::AbstractString)
     state_indent = state.indent
     boundaries = findall(text) do character
         character != '"'
@@ -350,24 +333,24 @@ function format_docstring(style, state, text)
             FootnoteRule(),
             MathRule(),
             TableRule(),
-            FormatRule(style, state),
+            FormatRule(style, state.opts),
         ],
     )(
         deindented_string,
     ))
     # Indent all non-first lines to match the current parser indent
-    state_indented = IOBuffer()
+    buf = IOBuffer()
     indent = " "^state_indent
     # This is the first line, so the rest have to be indented. A newline for it will be added below
-    write(state_indented, "\"\"\"")
+    write(buf, "\"\"\"")
     for line in split(formatted, '\n')
         # The last line will be empty and will turn into an indent, so no need to indent the last line below
-        write(state_indented, '\n')
-        write(state_indented, indent)
-        write(state_indented, line)
+        write(buf, '\n')
+        write(buf, indent)
+        write(buf, line)
     end
-    write(state_indented, "\"\"\"")
-    String(take!(state_indented))
+    write(buf, "\"\"\"")
+    String(take!(buf))
 end
 
 @inline function p_literal(
@@ -407,7 +390,7 @@ end
     startline, endline, str = str_info
     # @debug "" loc startline endline str
 
-    if from_docstring
+    if from_docstring && s.opts.format_docstrings
         str = format_docstring(ds, s, str)
     end
 
@@ -716,11 +699,11 @@ function p_functiondef(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
             add_node!(t, Whitespace(1), s)
             add_node!(t, pretty(style, cst[4], s), s, join_lines = true)
         else
-            s.indent += s.indent_size
+            s.indent += s.opts.indent_size
             n = pretty(style, cst[3], s, ignore_single_line = true)
             s.opts.always_use_return && prepend_return!(n, s)
-            add_node!(t, n, s, max_padding = s.indent_size)
-            s.indent -= s.indent_size
+            add_node!(t, n, s, max_padding = s.opts.indent_size)
+            s.indent -= s.opts.indent_size
             add_node!(t, pretty(style, cst[4], s), s)
         end
     else
@@ -749,13 +732,13 @@ function p_struct(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         add_node!(t, Whitespace(1), s)
         add_node!(t, pretty(style, cst[4], s), s, join_lines = true)
     else
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         n = pretty(style, cst[3], s, ignore_single_line = true)
         if s.opts.annotate_untyped_fields_with_any
             annotate_typefields_with_any!(n, s)
         end
-        add_node!(t, n, s, max_padding = s.indent_size)
-        s.indent -= s.indent_size
+        add_node!(t, n, s, max_padding = s.opts.indent_size)
+        s.indent -= s.opts.indent_size
         add_node!(t, pretty(style, cst[4], s), s)
     end
     t
@@ -776,13 +759,13 @@ function p_mutable(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         add_node!(t, Whitespace(1), s)
         add_node!(t, pretty(style, cst[5], s), s, join_lines = true)
     else
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         n = pretty(style, cst[4], s, ignore_single_line = true)
         if s.opts.annotate_untyped_fields_with_any
             annotate_typefields_with_any!(n, s)
         end
-        add_node!(t, n, s, max_padding = s.indent_size)
-        s.indent -= s.indent_size
+        add_node!(t, n, s, max_padding = s.opts.indent_size)
+        s.indent -= s.opts.indent_size
         add_node!(t, pretty(style, cst[5], s), s)
     end
     t
@@ -855,7 +838,7 @@ function p_toplevel(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
             s.offset += a.fullspan
             continue
         end
-        add_node!(t, pretty(style, a, s), s, max_padding = s.indent_size)
+        add_node!(t, pretty(style, a, s), s, max_padding = s.opts.indent_size)
         add_node!(t, Semicolon(), s)
     end
     t
@@ -872,14 +855,14 @@ function p_begin(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         add_node!(t, Whitespace(1), s)
         add_node!(t, pretty(style, cst[3], s), s, join_lines = true)
     else
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(
             t,
             pretty(style, cst[2], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size,
+            max_padding = s.opts.indent_size,
         )
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
         add_node!(t, pretty(style, cst[3], s), s)
     end
     t
@@ -897,14 +880,14 @@ function p_quote(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
             add_node!(t, Whitespace(1), s)
             add_node!(t, pretty(style, cst[3], s), s, join_lines = true)
         else
-            s.indent += s.indent_size
+            s.indent += s.opts.indent_size
             add_node!(
                 t,
                 pretty(style, cst[2], s, ignore_single_line = true),
                 s,
-                max_padding = s.indent_size,
+                max_padding = s.opts.indent_size,
             )
-            s.indent -= s.indent_size
+            s.indent -= s.opts.indent_size
             add_node!(t, pretty(style, cst[3], s), s)
         end
     else
@@ -934,23 +917,23 @@ function p_let(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
     add_node!(t, pretty(style, cst[1], s), s)
     if length(cst.args) > 3
         add_node!(t, Whitespace(1), s)
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         if cst[2].typ === CSTParser.Block
             add_node!(t, pretty(style, cst[2], s, join_body = true), s, join_lines = true)
         else
             add_node!(t, pretty(style, cst[2], s), s, join_lines = true)
         end
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
 
         idx = length(t.nodes)
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(
             t,
             pretty(style, cst[3], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size,
+            max_padding = s.opts.indent_size,
         )
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
         # Possible newline after args if nested to act as a separator
         # to the block body.
         if cst[2].typ === CSTParser.Block && t.nodes[end-2].typ !== NOTCODE
@@ -958,9 +941,9 @@ function p_let(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         end
         add_node!(t, pretty(style, cst.args[end], s), s)
     else
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(t, pretty(style, cst[2], s, ignore_single_line = true), s)
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
         add_node!(t, pretty(style, cst.args[end], s), s)
     end
     t
@@ -1017,21 +1000,21 @@ function p_for(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         eq_to_in_normalization!(cst[2], s.opts.always_for_in)
     end
     if cst[2].typ === CSTParser.Block
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(t, pretty(style, cst[2], s, join_body = true), s, join_lines = true)
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
     else
         add_node!(t, pretty(style, cst[2], s), s, join_lines = true)
     end
     idx = length(t.nodes)
-    s.indent += s.indent_size
+    s.indent += s.opts.indent_size
     add_node!(
         t,
         pretty(style, cst[3], s, ignore_single_line = true),
         s,
-        max_padding = s.indent_size,
+        max_padding = s.opts.indent_size,
     )
-    s.indent -= s.indent_size
+    s.indent -= s.opts.indent_size
 
     # Possible newline after args if nested to act as a separator
     # to the block body.
@@ -1060,11 +1043,11 @@ function p_do(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         add_node!(t, pretty(style, cst[3], s), s, join_lines = true)
     end
     if cst[4].typ === CSTParser.Block
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         n = pretty(style, cst[4], s, ignore_single_line = true)
         s.opts.always_use_return && prepend_return!(n, s)
-        add_node!(t, n, s, max_padding = s.indent_size)
-        s.indent -= s.indent_size
+        add_node!(t, n, s, max_padding = s.opts.indent_size)
+        s.indent -= s.opts.indent_size
     end
     add_node!(t, pretty(style, cst.args[end], s), s)
     t
@@ -1081,14 +1064,14 @@ function p_try(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         elseif a.typ === CSTParser.KEYWORD
             add_node!(t, pretty(style, a, s), s, max_padding = 0)
         elseif a.typ === CSTParser.Block
-            s.indent += s.indent_size
+            s.indent += s.opts.indent_size
             add_node!(
                 t,
                 pretty(style, a, s, ignore_single_line = true),
                 s,
-                max_padding = s.indent_size,
+                max_padding = s.opts.indent_size,
             )
-            s.indent -= s.indent_size
+            s.indent -= s.opts.indent_size
         else
             len = length(t)
             add_node!(t, Whitespace(1), s)
@@ -1111,14 +1094,14 @@ function p_if(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         add_node!(t, pretty(style, cst[1], s), s)
         add_node!(t, Whitespace(1), s)
         add_node!(t, pretty(style, cst[2], s), s, join_lines = true)
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(
             t,
             pretty(style, cst[3], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size,
+            max_padding = s.opts.indent_size,
         )
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
 
         len = length(t)
         if length(cst.args) > 4
@@ -1131,14 +1114,14 @@ function p_if(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
                 t.len = max(len, length(n))
             else
                 # ELSE KEYWORD
-                s.indent += s.indent_size
+                s.indent += s.opts.indent_size
                 add_node!(
                     t,
                     pretty(style, cst[5], s, ignore_single_line = true),
                     s,
-                    max_padding = s.indent_size,
+                    max_padding = s.opts.indent_size,
                 )
-                s.indent -= s.indent_size
+                s.indent -= s.opts.indent_size
             end
         end
         # END KEYWORD
@@ -1148,14 +1131,14 @@ function p_if(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         t.len += 7
         add_node!(t, pretty(style, cst[1], s), s)
 
-        s.indent += s.indent_size
+        s.indent += s.opts.indent_size
         add_node!(
             t,
             pretty(style, cst[2], s, ignore_single_line = true),
             s,
-            max_padding = s.indent_size,
+            max_padding = s.opts.indent_size,
         )
-        s.indent -= s.indent_size
+        s.indent -= s.opts.indent_size
 
         len = length(t)
         if length(cst.args) > 2
@@ -1169,14 +1152,14 @@ function p_if(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
                 # "elseif n"
                 t.len = max(len, length(n))
             else
-                s.indent += s.indent_size
+                s.indent += s.opts.indent_size
                 add_node!(
                     t,
                     pretty(style, cst[4], s, ignore_single_line = true),
                     s,
-                    max_padding = s.indent_size,
+                    max_padding = s.opts.indent_size,
                 )
-                s.indent -= s.indent_size
+                s.indent -= s.opts.indent_size
             end
         end
     end
@@ -1416,12 +1399,12 @@ function p_whereopcall(ds::DefaultStyle, cst::CSTParser.EXPR, s::State)
         if is_opener(a) && nest
             add_node!(t, pretty(style, a, s), s, join_lines = true)
             add_node!(t, Placeholder(0), s)
-            s.indent += s.indent_size
+            s.indent += s.opts.indent_size
         elseif is_closer(a) && nest
             add_node!(t, TrailingComma(), s)
             add_node!(t, Placeholder(0), s)
             add_node!(t, pretty(style, a, s), s, join_lines = true)
-            s.indent -= s.indent_size
+            s.indent -= s.opts.indent_size
         elseif CSTParser.is_comma(a) && !is_punc(cst[i+3])
             add_node!(t, pretty(style, a, s), s, join_lines = true)
             add_node!(t, Placeholder(nws), s)
diff --git a/src/print.jl b/src/print.jl
index 59437fb3..dc48a341 100644
--- a/src/print.jl
+++ b/src/print.jl
@@ -69,7 +69,7 @@ function print_tree(
             if notcode_indent > -1
                 n.indent = notcode_indent
             elseif i + 1 < length(nodes) && is_end(nodes[i+2])
-                n.indent += s.indent_size
+                n.indent += s.opts.indent_size
             elseif i + 1 < length(nodes) && (
                 nodes[i+2].typ === CSTParser.Block || nodes[i+2].typ === CSTParser.Begin
             )
diff --git a/src/state.jl b/src/state.jl
index 3e05e94d..de283fbd 100644
--- a/src/state.jl
+++ b/src/state.jl
@@ -1,17 +1,15 @@
 mutable struct State
     doc::Document
-    indent_size::Int
     indent::Int
     offset::Int
     line_offset::Int
-    margin::Int
 
     # If true, output is formatted text otherwise
     # it's source text
     on::Bool
     opts::Options
 end
-State(doc, indent_size, margin, opts) = State(doc, indent_size, 0, 1, 0, margin, true, opts)
+State(doc, opts) = State(doc, 0, 1, 0, true, opts)
 
 @inline nspaces(s::State) = s.indent
 @inline hascomment(d::Document, line::Integer) = haskey(d.comments, line)


Test patch:
diff --git a/test/config.jl b/test/config.jl
index 83d379d3..a604a2d5 100644
--- a/test/config.jl
+++ b/test/config.jl
@@ -4,8 +4,6 @@
     before = "begin rand() end\n"
     after2 = "begin\n  rand()\nend\n"
     after4 = "begin\n    rand()\nend\n"
-    before_markdown = "- 1\n- 2"
-    after_markdown = "  - 1\n  - 2\n"
 
     # test basic configuration case
     # test_basic_config
@@ -16,15 +14,11 @@
     try
         config_path = joinpath(sandbox_dir, CONFIG_FILE_NAME)
         code_path = joinpath(sandbox_dir, "code.jl")
-        markdown_path = joinpath(sandbox_dir, "markdown.md")
         open(io -> write(io, config2), config_path, "w")
         open(io -> write(io, before), code_path, "w")
-        open(io -> write(io, before_markdown), markdown_path, "w")
 
         @test format(code_path) == false
         @test read(code_path, String) == after2
-        @test format(markdown_path) == false
-        @test read(markdown_path, String) == after_markdown
     finally
         rm(sandbox_dir; recursive = true)
     end
@@ -74,16 +68,13 @@
         sub_dir = joinpath(sandbox_dir, "sub")
         mkdir(sub_dir)
         sub_code_path = joinpath(sub_dir, "sub_code.jl")
-        sub_markdown_path = joinpath(sub_dir, "sub_markdown.md")
         open(io -> write(io, config2), config_path, "w")
         open(io -> write(io, before), code_path, "w")
         open(io -> write(io, before), sub_code_path, "w")
-        open(io -> write(io, before_markdown), sub_markdown_path, "w")
 
         @test format(sandbox_dir) == false
         @test read(code_path, String) == after2
         @test read(sub_code_path, String) == after2
-        @test read(sub_markdown_path, String) == after_markdown
         @test format(sandbox_dir) == true
     finally
         rm(sandbox_dir; recursive = true)
@@ -166,4 +157,48 @@
         cd(original_dir)
         rm(sandbox_dir; recursive = true)
     end
+
+    config2 = """
+    indent = 2
+    format_markdown = true
+    """
+
+    before = """
+    # hello world
+
+    ```julia
+    begin body end
+    ```
+    - a
+    -             b
+    """
+    after2 = """
+    # hello world
+
+    ```julia
+    begin
+      body
+    end
+    ```
+
+      - a
+      -             b
+    """
+    # test formatting a markdown file
+    # test_basic_markdown_format
+    # ├─ .JuliaFormatter.toml (config2)
+    # └─ file.md (before -> after2)
+    sandbox_dir = joinpath(@__DIR__, "test_basic_config")
+    mkdir(sandbox_dir)
+    try
+        config_path = joinpath(sandbox_dir, CONFIG_FILE_NAME)
+        md_path = joinpath(sandbox_dir, "file.md")
+        open(io -> write(io, config2), config_path, "w")
+        open(io -> write(io, before), md_path, "w")
+
+        @test format(md_path) == false
+        @test read(md_path, String) == after2
+    finally
+        rm(sandbox_dir; recursive = true)
+    end
 end
diff --git a/test/default_style.jl b/test/default_style.jl
index ce5a5092..2887258d 100644
--- a/test/default_style.jl
+++ b/test/default_style.jl
@@ -1372,144 +1372,6 @@
 
     end
 
-    @testset "docs" begin
-        str = """
-        \"""
-        doc
-        \"""
-        function f()
-            20
-        end"""
-        t = run_pretty(str, 80)
-        @test length(t) == 12
-
-        normalized = """
-        \"""
-        doc
-        \"""
-        function f()
-            20
-        end"""
-
-        str = """
-        \"""doc
-        \"""
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        str = """
-        \"""
-        doc\"""
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        str = """
-        \"""doc\"""
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        str = """
-        "doc
-        "
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        str = """
-        "
-        doc"
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        str = """
-        "doc"
-        function f()
-            20
-        end"""
-        @test fmt(str) == normalized
-
-        # test aligning to function identation
-        str_ = """
-            "doc"
-        function f()
-            20
-        end"""
-        str = """
-        "doc"
-        function f()
-            20
-        end"""
-        @test fmt(str_) == normalized
-
-        str = """\"""
-                 doc for Foo
-                 \"""
-                 Foo"""
-        @test fmt(str) == str
-        t = run_pretty(str, 80)
-        @test length(t) == 11
-
-        str = """
-        \"""
-        doc
-        \"""
-        function f()    #  comment
-            20
-        end"""
-        @test fmt(str) == str
-
-        #
-        # Issue 157
-        str = raw"""
-        @doc \"""
-           foo()
-        \"""
-        foo() = bar()"""
-        @test fmt(str) == str
-
-        str = raw"""
-        @doc doc\"""
-           foo()
-        \"""
-        foo() = bar()"""
-        @test fmt(str) == str
-
-        str = raw"""@doc "doc for foo" foo"""
-        @test fmt(str) == str
-
-        str = raw"""@doc \"""doc for foo\""" foo"""
-        @test fmt(str) == str
-
-        str = raw"""@doc doc\"""doc for foo\""" foo()"""
-        @test fmt(str) == str
-
-        str = raw"""@doc foo"""
-        @test fmt(str) == str
-
-        # issue 160
-        str = """
-        module MyModule
-
-        import Markdown: @doc_str
-
-        @doc doc\"""
-            foo()
-        \"""
-        foo() = bar()
-
-        end # module"""
-        @test fmt(str) == str
-    end
-
     @testset "strings" begin
         str = """
         \"""
@@ -4804,152 +4666,6 @@ some_function(
         @test fmt(str_, 4, 1) == str
     end
 
-    @testset "Format docstrings" begin
-        unformatted = """
-        \"""
-        This is a docstring
-
-        ```@example
-        a =  1
-         b  = 2
-         a + b
-        ```
-
-        ```jldoctest
-        a =  1
-        b  = 2
-        a + b
-
-        # output
-
-        3
-        ```
-
-        ```jldoctest
-        julia> a =  1
-        1
-
-        julia> b  = 2;
-
-        julia>  a + b
-        3
-
-        julia> function test(x)
-               x + 1
-               x + 2
-               end;
-        ```
-        \"""
-        function test(x) x end"""
-
-        formatted = """
-        \"""
-        This is a docstring
-
-        ```@example
-        a = 1
-        b = 2
-        a + b
-        ```
-
-        ```jldoctest
-        a = 1
-        b = 2
-        a + b
-
-        # output
-
-        3
-        ```
-
-        ```jldoctest
-        julia> a = 1
-        1
-
-        julia> b = 2;
-
-
-        julia> a + b
-        3
-
-        julia> function test(x)
-                   x + 1
-                   x + 2
-               end;
-
-        ```
-        \"""
-        function test(x)
-            x
-        end"""
-        @test fmt(unformatted) == formatted
-    end
-    @testset "Multi-line indented code-blocks" begin
-        unformatted = """
-        \"""
-            fmt(
-            )
-        \"""
-        function fmt() end"""
-
-        formatted = """
-        \"""
-            fmt(
-            )
-        \"""
-        function fmt() end"""
-        @test fmt(unformatted) == formatted
-    end
-    @testset "Empty line in docstring" begin
-        unformatted = """
-        \"""
-
-        \"""
-        function test() end"""
-
-        formatted = """
-        \"""
-        \"""
-        function test() end"""
-        @test fmt(unformatted) == formatted
-    end
-    @testset "Indented docstring" begin
-        unformatted = """
-        begin
-            \"""
-            Indented docstring
-
-            with multiple paragraphs
-            \"""
-            indented_item
-        end"""
-        formatted = """
-        begin
-            \"""
-            Indented docstring
-
-            with multiple paragraphs
-            \"""
-            indented_item
-        end"""
-        @test fmt(unformatted) == formatted
-        short = """
-        begin
-            "Short docstring"
-            item
-        end
-        """
-        short_formatted = """
-        begin
-            \"""
-            Short docstring
-            \"""
-            item
-        end
-        """
-        @test fmt(short) == short_formatted
-    end
-
     @testset "issue 260 - BracesCat" begin
         str = "{1; 2; 3}"
         @test fmt(str, 4, length(str)) == str
diff --git a/test/options.jl b/test/options.jl
index d660b65e..6fef632d 100644
--- a/test/options.jl
+++ b/test/options.jl
@@ -109,7 +109,7 @@
         using .B: B
         using ...C: C"""
         @test fmt(str_, import_to_using = true) == str
-        t = run_pretty(str_, 80, opts = Options(import_to_using = true))
+        t = run_pretty(str_, opts = Options(max_margin = 80, import_to_using = true))
         @test t.len == 13
 
         # #232
@@ -353,7 +353,10 @@
             end"""
         @test fmt(str_, annotate_untyped_fields_with_any = false) == str
 
-        t = run_pretty(str_, 80, opts = Options(annotate_untyped_fields_with_any = false))
+        t = run_pretty(
+            str_,
+            opts = Options(max_margin = 80, annotate_untyped_fields_with_any = false),
+        )
         @test length(t) == 11
 
         str = """
@@ -379,8 +382,311 @@
             end"""
         @test fmt(str_, annotate_untyped_fields_with_any = false) == str
 
-        t = run_pretty(str_, 80, opts = Options(annotate_untyped_fields_with_any = false))
+        t = run_pretty(
+            str_,
+            opts = Options(max_margin = 80, annotate_untyped_fields_with_any = false),
+        )
         @test length(t) == 23
     end
 
+    @testset "format docstrings - basic" begin
+        str = """
+        \"""
+        doc
+        \"""
+        function f()
+            20
+        end"""
+        t = run_pretty(str, 80)
+        @test length(t) == 12
+
+        normalized = """
+        \"""
+        doc
+        \"""
+        function f()
+            20
+        end"""
+
+        str = """
+        \"""doc
+        \"""
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        str = """
+        \"""
+        doc\"""
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        str = """
+        \"""doc\"""
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        str = """
+        "doc
+        "
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        str = """
+        "
+        doc"
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        str = """
+        "doc"
+        function f()
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == normalized
+
+        # test aligning to function identation
+        str_ = """
+            "doc"
+        function f()
+            20
+        end"""
+        str = """
+        "doc"
+        function f()
+            20
+        end"""
+        @test fmt(str_) == str
+        @test fmt(str_, format_docstrings = true) == normalized
+
+        str = """\"""
+                 doc for Foo
+                 \"""
+                 Foo"""
+        @test fmt(str) == str
+        t = run_pretty(str, 80)
+        @test length(t) == 11
+
+        str = """
+        \"""
+        doc
+        \"""
+        function f()    #  comment
+            20
+        end"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        # Issue 157
+        str = raw"""
+        @doc \"""
+           foo()
+        \"""
+        foo() = bar()"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        str = raw"""
+        @doc doc\"""
+           foo()
+        \"""
+        foo() = bar()"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        str = raw"""@doc "doc for foo" foo"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        str = raw"""@doc \"""doc for foo\""" foo"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        str = raw"""@doc doc\"""doc for foo\""" foo()"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        str = raw"""@doc foo"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+
+        # issue 160
+        str = """
+        module MyModule
+
+        import Markdown: @doc_str
+
+        @doc doc\"""
+            foo()
+        \"""
+        foo() = bar()
+
+        end # module"""
+        @test fmt(str) == str
+        @test fmt(str, format_docstrings = true) == str
+    end
+
+    @testset "format docstrings - with code" begin
+        unformatted = """
+        \"""
+        This is a docstring
+
+        ```@example
+        a =  1
+         b  = 2
+         a + b
+        ```
+
+        ```jldoctest
+        a =  1
+        b  = 2
+        a + b
+
+        # output
+
+        3
+        ```
+
+        ```jldoctest
+        julia> a =  1
+        1
+
+        julia> b  = 2;
+
+        julia>  a + b
+        3
+
+        julia> function test(x)
+               x + 1
+               x + 2
+               end;
+        ```
+        \"""
+        function test(x) x end"""
+
+        formatted = """
+        \"""
+        This is a docstring
+
+        ```@example
+        a = 1
+        b = 2
+        a + b
+        ```
+
+        ```jldoctest
+        a = 1
+        b = 2
+        a + b
+
+        # output
+
+        3
+        ```
+
+        ```jldoctest
+        julia> a = 1
+        1
+
+        julia> b = 2;
+
+
+        julia> a + b
+        3
+
+        julia> function test(x)
+                   x + 1
+                   x + 2
+               end;
+
+        ```
+        \"""
+        function test(x)
+            x
+        end"""
+        @test fmt(unformatted, format_docstrings = true) == formatted
+    end
+
+    @testset "format docstrings - Multi-line indented code-blocks" begin
+        unformatted = """
+        \"""
+            fmt(
+            )
+        \"""
+        function fmt() end"""
+
+        formatted = """
+        \"""
+            fmt(
+            )
+        \"""
+        function fmt() end"""
+        @test fmt(unformatted, format_docstrings = true) == formatted
+    end
+    @testset "format docstrings - Empty line in docstring" begin
+        unformatted = """
+        \"""
+
+        \"""
+        function test() end"""
+
+        formatted = """
+        \"""
+        \"""
+        function test() end"""
+        @test fmt(unformatted, format_docstrings = true) == formatted
+    end
+
+    @testset "format docstrings - Indented docstring" begin
+        unformatted = """
+        begin
+            \"""
+            Indented docstring
+
+            with multiple paragraphs
+            \"""
+            indented_item
+        end"""
+        formatted = """
+        begin
+            \"""
+            Indented docstring
+
+            with multiple paragraphs
+            \"""
+            indented_item
+        end"""
+        @test fmt(unformatted, format_docstrings = true) == formatted
+
+        short = """
+        begin
+            "Short docstring"
+            item
+        end
+        """
+        short_formatted = """
+        begin
+            \"""
+            Short docstring
+            \"""
+            item
+        end
+        """
+        @test fmt(short, format_docstrings = true) == short_formatted
+    end
 end
diff --git a/test/runtests.jl b/test/runtests.jl
index de83f1c8..300f3602 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -15,27 +15,26 @@ function fmt(s; i = 4, m = 80, kwargs...)
 end
 fmt(s, i, m; kwargs...) = fmt(s; kwargs..., i = i, m = m)
 
-function run_pretty(
-    text::String,
-    print_width::Int;
-    opts = Options(),
-    style = DefaultStyle(),
-)
+function run_pretty(text::String; style = DefaultStyle(), opts = Options())
     d = JuliaFormatter.Document(text)
-    s = JuliaFormatter.State(d, 4, print_width, opts)
+    s = JuliaFormatter.State(d, opts)
     x = CSTParser.parse(text, true)
     t = JuliaFormatter.pretty(style, x, s)
     t
 end
+run_pretty(text::String, max_margin::Int) =
+    run_pretty(text, opts = Options(max_margin = max_margin))
 
-function run_nest(text::String, print_width::Int; opts = Options(), style = DefaultStyle())
+function run_nest(text::String; opts = Options(), style = DefaultStyle())
     d = JuliaFormatter.Document(text)
-    s = JuliaFormatter.State(d, 4, print_width, opts)
+    s = JuliaFormatter.State(d, opts)
     x = CSTParser.parse(text, true)
     t = JuliaFormatter.pretty(style, x, s)
     JuliaFormatter.nest!(style, t, s)
     t, s
 end
+run_nest(text::String, max_margin::Int) =
+    run_nest(text, opts = Options(max_margin = max_margin))
 
 yasfmt1(s, i, m; kwargs...) = fmt1(s; kwargs..., i = i, m = m, style = YASStyle())
 yasfmt(s, i, m; kwargs...) = fmt(s; kwargs..., i = i, m = m, style = YASStyle())