Class: ProseMirror::Serializers::MarkdownSerializerState

Inherits:
Object
  • Object
show all
Defined in:
lib/prose_mirror/serializers/markdown_serializer.rb

Overview

This class is used to track state and expose methods related to markdown serialization

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(nodes, marks, options) ⇒ MarkdownSerializerState

Initialize a new state object


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 335

def initialize(nodes, marks, options)
  @nodes = nodes
  @marks = marks
  @options = options
  @delim = ""
  @out = ""
  @closed = nil
  @in_autolink = nil
  @at_block_start = false
  @in_tight_list = false
  @in_blockquote = false

  @options[:tight_lists] = false if @options[:tight_lists].nil?
  @options[:hard_break_node_name] = "hard_break" if @options[:hard_break_node_name].nil?
end

Instance Attribute Details

#at_block_startObject

Returns the value of attribute at_block_start.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def at_block_start
  @at_block_start
end

#closedObject

Returns the value of attribute closed.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def closed
  @closed
end

#delimObject

Returns the value of attribute delim.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def delim
  @delim
end

Returns the value of attribute in_autolink.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def in_autolink
  @in_autolink
end

#in_blockquoteObject

Returns the value of attribute in_blockquote.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def in_blockquote
  @in_blockquote
end

#in_tight_listObject

Returns the value of attribute in_tight_list.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def in_tight_list
  @in_tight_list
end

#marksObject (readonly)

Returns the value of attribute marks.


332
333
334
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 332

def marks
  @marks
end

#nodesObject (readonly)

Returns the value of attribute nodes.


332
333
334
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 332

def nodes
  @nodes
end

#optionsObject (readonly)

Returns the value of attribute options.


332
333
334
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 332

def options
  @options
end

#outObject

Returns the value of attribute out.


331
332
333
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 331

def out
  @out
end

Instance Method Details

#at_blank?Boolean

Check if the output ends with a blank line

Returns:

  • (Boolean)

391
392
393
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 391

def at_blank?
  /(^|\n)$/.match?(@out)
end

#close_block(node) ⇒ Object

Close the block for the given node


408
409
410
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 408

def close_block(node)
  @closed = node
end

#ensure_new_lineObject

Ensure the current content ends with a newline


396
397
398
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 396

def ensure_new_line
  @out += "\n" unless at_blank?
end

#esc(str, start_of_line = false) ⇒ Object

Escape Markdown characters


647
648
649
650
651
652
653
654
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 647

def esc(str, start_of_line = false)
  str = str.gsub(/[`*\\~\[\]_]/) { |m| "\\" + m }
  if start_of_line
    str = str.gsub(/^[#\-*+>]/) { |m| "\\" + m }
      .gsub(/^(\d+)\./) { |match, d| d.to_s + "\\." }
  end
  str.gsub("![", "\\![")
end

#flush_close(size = 2) ⇒ Object

Flush any pending closing operations


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 352

def flush_close(size = 2)
  if @closed
    @out += "\n" unless at_blank?
    if size > 1
      delim_min = @delim
      trim = /\s+$/.match(delim_min)
      delim_min = delim_min[0...delim_min.length - trim[0].length] if trim

      (1...size).each do
        @out += delim_min + "\n"
      end
    end
    @closed = nil
  end
end

#get_mark(name) ⇒ Object

Get mark info by name


369
370
371
372
373
374
375
376
377
378
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 369

def get_mark(name)
  info = @marks[name.to_sym]
  if !info
    if @options[:strict] != false
      raise "Mark type `#{name}` not supported by Markdown renderer"
    end
    info = BLANK_MARK
  end
  info
end

#render(node, parent, index) ⇒ Object

Render a node


429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 429

def render(node, parent, index)
  if @nodes[node.type.name.to_sym]
    @nodes[node.type.name.to_sym].call(self, node, parent, index)
  elsif @options[:strict] != false
    raise "Token type `#{node.type.name}` not supported by Markdown renderer"
  elsif !node.type.is_leaf
    if node.type.inline_content
      render_inline(node)
    else
      render_content(node)
    end
    close_block(node) if node.is_block
  end
end

#render_content(parent) ⇒ Object

Render the contents of a parent as block nodes


445
446
447
448
449
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 445

def render_content(parent)
  parent.each_with_index do |node, i|
    render(node, parent, i)
  end
end

#render_inline(parent, from_block_start = true) ⇒ Object

Render inline content


452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 452

def render_inline(parent, from_block_start = true)
  @at_block_start = from_block_start
  active = []
  trailing = ""

  progress = lambda do |node, offset, index|
    marks = node ? node.marks : []

    # Remove marks from hard_break that are the last node inside
    # that mark to prevent parser edge cases
    if node && node.type.name == @options[:hard_break_node_name]
      marks = marks.select do |m|
        next false if index + 1 == parent.child_count

        next_node = parent.child(index + 1)
        m.is_in_set(next_node.marks) && (!next_node.is_text || /\S/.match?(next_node.text))
      end
    end

    leading = trailing
    trailing = ""

    # Handle whitespace expelling
    if node && node.is_text && marks.any? { |mark|
      info = get_mark(mark.type.name.to_sym)
      info && info[:expel_enclosing_whitespace] && !mark.is_in_set(active)
    }
      match = /^(\s*)(.*)$/m.match(node.text)
      if match[1] && !match[1].empty?
        leading += match[1]
        node = (match[2] && !match[2].empty?) ? node.with_text(match[2]) : nil
        marks = active if node.nil?
      end
    end

    if node && node.is_text && marks.any? { |mark|
      info = get_mark(mark.type.name.to_sym)
      info && info[:expel_enclosing_whitespace] &&
          (index == parent.child_count - 1 || !mark.is_in_set(parent.child(index + 1).marks))
    }
      match = /^(.*?)(\s*)$/m.match(node.text)
      if match[2] && !match[2].empty?
        trailing = match[2]
        node = (match[1] && !match[1].empty?) ? node.with_text(match[1]) : nil
        marks = active if node.nil?
      end
    end

    inner = (marks.length > 0) ? marks[marks.length - 1] : nil
    no_esc = inner && get_mark(inner.type.name)[:escape] == false
    len = marks.length - (no_esc ? 1 : 0)

    # Try to reorder marks to avoid needless mark recalculation
    i = 0
    while i < len
      mark = marks[i]
      info = get_mark(mark.type.name)
      break unless info[:mixable]

      j = 0
      while j < active.length
        other = active[j]
        info_other = get_mark(other.type.name)
        break unless info_other[:mixable]

        if mark.eq(other)
          if i > j
            marks = marks[0...j] + [mark] + marks[j...i] + marks[(i + 1)...len]
          elsif j > i
            marks = marks[0...i] + marks[(i + 1)...j] + [mark] + marks[j...len]
          end
          break
        end
        j += 1
      end
      i += 1
    end

    # Find the prefix of the mark set that didn't change
    keep = 0
    while keep < [active.length, len].min && active[keep].eq(marks[keep])
      keep += 1
    end

    # Close marks that no longer apply
    (active.length - 1).downto(keep) do |i|
      info = get_mark(active[i].type.name)
      text = info[:close]
      text = text.call(self, active[i], parent, index) if text.is_a?(Proc)
      write(text)
    end

    text = leading
    active.slice!(keep, active.length)

    # Output any previously expelled trailing whitespace
    if text && leading.length > 0
      @out += text
    end

    # Open marks that are new now
    (keep...len).each do |i|
      info = get_mark(marks[i].type.name)
      text = info[:open]
      text = text.call(self, marks[i], parent, index) if text.is_a?(Proc)
      write(text)
      active.push(marks[i])
    end

    # Render the node
    if node
      write
      if no_esc
        render(node, parent, index)
      else
        text = node.text
        if node.is_text
          write(esc(text, @at_block_start))
        else
          render(node, parent, index)
        end
      end
    end

    @at_block_start = false
  end

  (0...parent.child_count).each do |i|
    progress.call(parent.child(i), 0, i)
  end
  progress.call(nil, 0, parent.child_count)
end

#render_list(node, indent, marker_gen, in_blockquote = false) ⇒ Object

Render a list


586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 586

def render_list(node, indent, marker_gen, in_blockquote = false)
  # Clear any closed block
  if @closed && @closed.type == node.type
    @closed = nil
  else
    ensure_new_line
  end

  # Remember current indentation level
  old_indent = @delim
  is_nested = !old_indent.empty?

  # Set up tight list handling
  old_tight = @in_tight_list
  @in_tight_list = node.attrs[:tight]

  # Process each list item
  node.each_with_index do |child, i|
    # For blockquote lists, add the blockquote prefix
    @delim = if in_blockquote
      "> "
    # For nested lists, add standard indentation
    elsif is_nested
      "    "
    else
      ""
    end

    # Write the marker with proper spacing
    write(marker_gen.call(i))

    # Store the current position after the marker
    old_delim = @delim

    # Add exactly one space after the marker for content
    @delim = old_delim + " "

    # Render the item's content
    render(child, node, i)

    # Restore delimiter for next item
    @delim = old_delim

    # Add a newline after each list item unless it's the last one
    unless i == node.child_count - 1
      ensure_new_line
    end
  end

  # Restore the tight list setting
  @in_tight_list = old_tight

  # Restore the original indentation
  @delim = old_indent

  # Handle spacing between list items and the next content
  size = (@closed && @closed.type.name == "paragraph" && !node.attrs[:tight]) ? 2 : 1
  flush_close(size)
end

#repeat(str, n) ⇒ Object

Repeat a string n times


657
658
659
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 657

def repeat(str, n)
  str * n.to_i
end

#text(text, escape = true) ⇒ Object

Add text to the document


413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 413

def text(text, escape = true)
  lines = text.split("\n")
  lines.each_with_index do |line, i|
    write

    # Escape exclamation marks in front of links
    if !escape && line[0] == "[" && /(^|[^\\])!$/.match?(@out)
      @out = @out[0...@out.length - 1] + "\\!"
    end

    @out += escape ? esc(line, @at_block_start) : line
    @out += "\n" if i != lines.length - 1
  end
end

#wrap_block(delim, first_delim, node) ⇒ Object

Wrap a block with delimiters


381
382
383
384
385
386
387
388
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 381

def wrap_block(delim, first_delim, node)
  old = @delim
  write(first_delim.nil? ? delim : first_delim)
  @delim += delim
  yield
  @delim = old
  close_block(node)
end

#write(content = nil) ⇒ Object

Write content to the output


401
402
403
404
405
# File 'lib/prose_mirror/serializers/markdown_serializer.rb', line 401

def write(content = nil)
  flush_close
  @out += @delim if @delim && at_blank?
  @out += content if content
end