package provide next::doc 0.1
package require next
#
# Study for documentation classes for Next.
#
# Compared to the "old" @ docmentation effort, this is a rather
# light-weight structure based on xotcl 2 language features. The
# documentation classes build an (extensible) object structure which
# is used as a basis for some renderers. In general, the classes are
# defined in a way they can be used for
#
# a) building documentation outside the source code artefacts, or
#
# b) inside code artefacts (value added method definition commands
# providing extra arguments for the documentation). The
# documentation commands could reuse there names/arguments
# etc. directly from the method definition by issueing these
# commands inside the method definition methods.
#
# One could provide lint-like features to signal, whether the
# documentation is in sync with actually defined methods (when these
# are available).
#
namespace eval ::nx::doc {
namespace import -force ::nx::*
#
# A few helper commands:
# - "@" is a conveniant way for creating new objects with less syntactic overhead
# - "sorted" is used to sort instances by values of a specified attribute
#
proc @ {class name args} {$class new -name $name {*}$args}
proc sorted {instances sortedBy} {
set order [list]
foreach v $instances {lappend order [list $v [$v eval [list set :$sortedBy]]]}
set result [list]
foreach pair [lsort -index 1 $order] {lappend result [lindex $pair 0]}
return $result
}
Class create EntityFactory -superclass Class {
#
# EntityFactory is a meta-class for named doc entities
#
:attribute tag:required
namespace eval ::nx::doc::entities {}
set :root_namespace ::nx::doc::entities
:method init {} {
next
[:info class] object forward @${:tag} [self] new -name %1
}
:method createOrConfigure {id arguments} {
namespace eval $id {}
if {[::nx::core::objectproperty $id object]} {
$id configure {*}$arguments
} else {
:create $id {*}$arguments
}
return $id
}
}
Class create Entity {
#
# Entity is the base class for the documentation classes
#
# every Entity must be created with a "doc" value and can have
# an optional initcmd
:method objectparameter args {next {doc:optional __initcmd:initcmd,optional}}
:attribute doc:multivalued {set :incremental 1}
#the following two cases (incremental multivalued) could be nicer
:attribute {variants:multivalued ""} {set :incremental 1}
:attribute {params:multivalued ""} {set :incremental 1}
:attribute {@see:multivalued ""} {set :incremental 1}
# @method _doc
#
# The method _doc can be use to obtain the value of the documentation
# from another doc entity. This should avoid redundant documentation pieces.
:method _doc {doc use what value} {
if {$doc ne ""} {return $doc}
if {$use ne ""} {
foreach thing {NextCommand NextClass} {
set docobj [$thing id $use]
if {[::nx::core::objectproperty $docobj object]} break
}
if {[::nx::core::objectproperty $docobj object]} {
if {![$docobj exists $what]} {error "no attribute $what in $docobj"}
set names [list]
foreach v [$docobj $what] {
if {[$v name] eq $value} {return [$v doc]}
lappend names [$v name]
}
error "can't use $use, no $what with name $value in $docobj (available: $names)"
} else {
error "can't use $use, no documentation object $docobj"
}
}
}
# @method process
#
# This is an abstract hook method to be refined by the subclasses
# of Entity
:method process {comment_block} {
puts stderr "EntityFactory process -context [self] $comment_block"
EntityFactory process -context [self] $comment_block
}
# @method param
#
# The method param is currently used for documenting parameters of
# tcl-commands and xotcl methods. Most probably, it should cover
# object parameters as well. The parameters are identified by a
# name and ar part of another documentation entitiy
#
:method param {param doc {-use ""}} {
set flags [list -param $param]
if {[llength $param]>1} {
lappend flags -default [lindex $param 1]
set param [lindex $param 0]
}
set name $param
if {[regexp {^(.*):(.*)$} $param _ name spec]} {
lappend flags -spec $spec
}
lappend flags -fullname param
@ NextCommand::Parameter $name -partof [self] {*}$flags [:_doc $doc $use params $name]
}
# @method variant
#
# variants are used in cases, where depending on a parameter, the
# semantics of a command (and therefore its documentation) is
# completely different. A typical case are subcommands in Tcl.
#
:method variant {name doc {-use ""}} {
@ NextCommand::Variant $name -partof [self] [:_doc $doc $use variants $name]
}
# @method text
#
# text is used to access the content of doc of an Entity, and
# performs substitution on it. The substitution is not essential,
# but looks for now convenient.
#
:method text {} {subst [join ${:doc} " "]}
}
#
# Now, define some kinds of docEntities. The toplevel docEntities
# are named objects in the ::nx::doc::entities namespace to ease access to it.
#
# We define here the following toplevel docEntities (e.g. xotclObject will follow):
# - NextCommand
# - NextObject
#
# The xotcl methods are defined as Parts.
# - NextMethod
#
EntityFactory create NextCommand \
-tag "command" \
-superclass Entity {
:attribute name
:attribute arguments
:attribute {returns ""}
:object method id {name} {return [[:info class] eval {set :root_namespace}]::cmd::[string trimleft $name :]}
:object method new args {
foreach {att value} $args {if {$att eq "-name"} {set name $value}}
:createOrConfigure [:id $name] $args
}
}
EntityFactory create NextClass \
-tag "class" \
-superclass Entity {
:attribute name
:attribute {@author:multivalued ""} {
# TODO: incremental does not produced effects apart from
# deactivating the optimizer, shouldn't set the attribute's
# default methods to {get add}, to obtain the increment
# effect?
set :incremental 1
}
:attribute {methods:multivalued ""} {set :incremental 1}
:object method id {name} {puts stderr ""; return [[:info class] eval {set :root_namespace}]::class::[string trimleft $name :]}
:object method new args {
foreach {att value} $args {if {$att eq "-name"} {set name $value}}
:createOrConfigure [:id $name] $args
}
}
# @class Part
#
# A Part is a part of a documentation entity, defined by a
# separate object. Every Part is associated to another
# documentation entity and is identified by a name.
#
Class create Part -superclass Entity {
#:method objectparameter args {next {doc -use}}
:attribute name:required
:attribute partof:required
:attribute use
}
#
# variant and param are Parts:
#
Class create NextCommand::Variant -superclass Part {
:method init {} {${:partof} variants add [self] end}
}
Class create NextCommand::Parameter -superclass Part {
:attribute param
:attribute fullname
:attribute spec
:attribute default
:method init {} {${:partof} params add [self] end}
}
#
# xotclMethod is a named entity, which is part of some other
# docEntity (a class or an object). We might be able to use the
# "use" parameter for registered aliases to be able to refer to the
# documentation of the original method.
#
EntityFactory create NextMethod \
-tag "method" \
-superclass Part {
:attribute {scope class}
:attribute {modifier public}
:attribute arguments
:attribute {returns ""}
:object method id {partof scope name} {
return [[:info class] eval {set :root_namespace}]::method::[string trimleft $partof :]::${scope}::${name}
}
:object method new args {
foreach {att value} $args {
if {$att eq "-partof"} {set partof $value}
if {$att eq "-name"} {set name $value}
if {$att eq "-scope"} {set scope $value}
}
if {![info exists scope]} {
if {[::nx::core::objectproperty $partof class]} {
set scope class
} elseif {[::nx::core::objectproperty $partof object]} {
set scope object
} else {
set scope class
}
}
:createOrConfigure [:id $partof $scope $name] $args
}
:method init {} {[NextClass id ${:partof}] methods add [self] end}
:method signature {} {
if {[info exists :arguments]} {
set arguments ${:arguments}
} else {
set arguments [list]
foreach p [:params] {lappend arguments [$p param]}
}
set result "obj ${:name} $arguments"
}
}; # NextMethod
namespace export EntityFactory NextCommand NextClass NextMethod @
}
namespace eval ::nx::doc {
#
# Provide a simple HTML renderer. For now, we make our life simple
# by defining for the different supported docEntities different methods.
#
# We could think about a java-doc style renderer...
#
Class create HTMLrenderer {
# render command pieces in the text
:method cmd {text} {return <@TT>$text@TT>}
#
# render xotcl commands
#
:method renderCmd {} {
puts "
[:cmd ${:name}]
\n[:text]"
set variants [sorted [:variants] name]
if {$variants ne ""} {
puts " "
foreach v $variants {puts " - [$v text]"}
puts "
"
}
set params [:params]
if {$params ne ""} {
puts " "
foreach v $params {puts " - [$v cmd [$v name]] [$v text]"}
puts "
"
}
puts "\n"
}
#
# render xotcl classes
#
:method renderClass {} {
puts "[:cmd ${:name}]
\n[:text]"
set methods [sorted [:methods] name]
if {$methods ne ""} {
puts "
Methods of ${:name}:\n "
foreach v $methods {if {[$v scope] eq "class"} {$v renderMethod}}
puts "
"
puts "
Object Methods of ${:name}:\n "
foreach v $methods {if {[$v scope] eq "object"} {$v renderMethod}}
puts "
"
}
puts "\n"
}
#
# render xotcl methods
#
:method renderMethod {} {
puts "[:cmd [:signature]]
\n[:text]"
set params [:params]
if {$params ne ""} {
puts " "
foreach v $params {puts " - [$v cmd [$v name]] [$v text]"}
puts "
"
}
if {${:returns} ne ""} {
puts " Returns: ${:returns}"
}
puts "\n"
}
}
}
#
# post processor for initcmds and method bodies
#
namespace eval ::nx {
namespace import -force ::nx::doc::*
Object create doc {
:method log {msg} {
puts stderr "[self]->[uplevel 1 [list ::nx::core::current proc]]: $msg"
}
# @method process
#
# There is a major distinction: Is the entity the comment block is
# referring to given *extrinsically* (to the comment block) or
# *intrinsically* (as a starting tag).
#
# a. extrinsic: 'thing' is a valid class or object name
# b. intrinsic: 'thing' is a arbitrary string block describing
# a script.
#
:method process {thing} {
# TODO: tcl packages as an option?
# 1) in-situ processing: a class object
if {[::nx::core::objectproperty $thing class]} {
if {[$thing exists __initcmd]} {
:analyze_initcmd NextClass $thing [$thing eval {set :__initcmd}]
}
} elseif {[::nx::core::objectproperty $thing object]} {
# 2) in-situ processing: a non-class object
:log "can't postprocess objects currently"
} elseif {[file isfile $thing]} {
# 3) alien script file
if {[file isreadable $thing]} {
set fh [open $thing r]
if {[catch {set script [read $thing]} msg]} {
:log "error reading the file '$thing', i.e.: '$msg'"
}
} else {
:log "file '$thing' not readable"
}
} else {
# 4) we assume a string block, e.g., to be fed into eval
:analyze $thing
}
}
:method analyze {script} {
set blocks [:comment_blocks $script]
:log "blocks: '$blocks'"
foreach block $blocks {
:log "block: '$block'"
set analyzed_block [:analyze_comment_block $block]
set cb [dict create {*}$analyzed_block]
:log ">>>> $cb"
#
# 1) resolve the entity by tag; e.g.: class -> NextClass
#
set entity [EntityFactory eval set :tags([dict get $cb entity])]
#
# 2) provide an object rep of the entity
#
set entity_instance [@ $entity [dict get $cb [$entity tag]] [dict get $cb text]]
#
# 3) process entity-specific parts, according to their tags
#
$entity_instance process $analyzed_block
}
}
:method analyze_line {line} {
if {[regexp {^\s*$} $line]} {
return 1
} elseif {[regexp {^\s*#} $line]} {
return 2
} else {
return 3
}
}
:method analyze_line {line} {
#
# 1 ... empty line
#
if {[regexp {^\s*$} $line]} {
return 1
} elseif {[regexp {^\s*#\s*@[^[:space:]@]} $line]} {
#
# 2 ... tagged comment line
#
return 2
} elseif {[regexp {^\s*#\s*[^[:space:]]\s*} $line]} {
#
# 3 ... untagged, non-emtpy comment line
#
return 3
} elseif {[regexp {^\s*#} $line]} {
#
# 4 ... untagged, empty comment line
#
return 4
} else {
#
# 5 ... code line
#
return 5
}
}
:method analyze_line {line} {
if {[regexp -- {^\s*#+[#\s]*(.*)$} $line --> comment]} {
return [list 1 [string trim $comment]]
} else {
return [list 0 $line]
}
}
:method append_tag {line} {
set line [:remove_comment_markup $line]
set tag [string trimleft [lindex $line 0] @]
return [list $tag [lrange $line 1 end]]
}
:method comment_blocks {script} {
set lines [split $script \n]
set comment_blocks [list]
set was_comment 0
set spec {
0,1 {set comment_block [list]; lappend comment_block $text}
1,0 {lappend comment_blocks $comment_block}
1,1 {lappend comment_block $text}
0,0 {}
}
array set do $spec
foreach line $lines {
foreach {is_comment text} [:analyze_line $line] break;
eval $do($was_comment,$is_comment)
set was_comment $is_comment
}
return $comment_blocks
}
:method remove_comment_markup {comment} {
regsub -all -line {^\s*#} $comment "" comment
return $comment
}
:method analyze_comment_block {comment} {
set result [list]
set text ""
foreach line [split $comment \n] {
if {[regexp {^ *@(class|attribute|param|returns|method|object-method) (.*)$} $line _ kind value]} {
if {$kind eq "param"} {
if {[regexp {^\s*(\S+)\s+(.*)$} $value _ name desc]} {
set value [list $name $desc]
} else {
puts stderr "invalid param specification $value"
}
}
lappend result entity $kind
lappend result $kind $value
} else {
append text $line
}
}
lappend result text $text
#puts result=$result
return $result
}
:method analyze_method_block {-methodName -partof -scope -arguments analyzed_block} {
array set cb $analyzed_block
@ NextMethod $methodName -partof $partof -scope $scope $cb(text)
set m [NextMethod id $partof $scope $methodName]
set docparams [list]
foreach {att value} $analyzed_block {
# we do not handle "use" yet
if {$att eq "param"} {
$m param [lindex $value 0] [lindex $value 1]
lappend docparams [lindex $value 0]
} elseif {$att eq "returns"} {
$m returns $value
}
}
if {$arguments eq ""} {
set arguments $docparams
}
$m arguments $arguments
}
:method analyze_body {-partof -methodName -scope arguments body} {
set blocks [:comment_blocks -mode first $body]
if {[llength $blocks] > 0} {
:analyze_method_block -methodName $methodName -partof $partof -scope $scope \
-arguments $arguments \
[:analyze_comment_block [lindex $blocks 0]]
}
}
:method analyze_initcmd {docKind name initcmd} {
set first_block 1
foreach block [:comment_blocks $initcmd] {
if {$first_block} {
set id [@ $docKind $name]
if {[catch {$id process $block} msg]} {
puts stderr $msg
}
}
set first_block 0
}
}; # analyze_initcmd method
# :method analyze_initcmd {docKind name initcmd} {
# set first_block 1
# foreach block [:comment_blocks $initcmd] {
# set analyzed_block [:analyze_comment_block $block]
# array unset cb
# array set cb $analyzed_block
# if {$first_block} {
# set first_block 0
# if {[array size cb] == 1} {
# # we got a comment for the doc kind
# @ $docKind $name $cb(text)
# continue
# }
# }
# if {[info exists cb(method)] || [info exists cb(object-method)]} {
# set arguments ""
# if {[info exists cb(method)]} {
# set methodName $cb(method)
# set scope class
# catch {set arguments [$name info method args $methodName]}
# } else {
# set methodName $cb(object-method)
# set scope object
# catch {set arguments [$name object info method args $methodName]}
# }
# :analyze_method_block -methodName $methodName -partof $name -scope $scope \
# -arguments $arguments $analyzed_block
# }
# }
# foreach methodName [$name info methods -methodtype scripted] {
# :analyze_body -partof $name -methodName $methodName -scope class \
# [$name info method args $methodName] \
# [$name info method body $methodName]
# }
# foreach methodName [$name object info methods -methodtype scripted] {
# :analyze_body -partof $name -methodName $methodName -scope object \
# [$name object info method args $methodName] \
# [$name object info method body $methodName]
# }
# }; # analyze_initcmd method
# activate the recoding of initcmads
::nx::core::configure keepinitcmd true
}
}
#
# toplevel interface
# ::nx::doc::make all
# ::nx::doc::make doc
#
namespace eval ::nx::doc {
Object create make {
:method all {{-verbose:switch} {-class ::nx::Class}} {
foreach c [$class info instances -closure] {
if {$verbose} {puts "postprocess $c"}
::nx::doc::postprocessor process $c
}
}
:method doc {{-renderer ::nx::doc::HTMLrenderer}} {
# register the HTML renderer for all docEntities.
Entity mixin add $renderer
puts "Primitive XOTcl framework commands
\n"
foreach cmd [sorted [NextCommand info instances] name] {
$cmd renderCmd
}
puts "
\n\n"
puts "XOTcl Classes
\n"
foreach cmd [sorted [NextClass info instances] name] {
$cmd renderClass
}
puts "
\n\n"
Entity mixin delete $renderer
}
}
#
# modal comment block parsing
#
#
# contexts are entities
#
EntityFactory eval {
:object forward has_next expr {${:idx} < [llength ${:comment_block}]}
:object method dequeue {} {
set r [lindex ${:comment_block} ${:idx}]
incr :idx
return $r
}
:object forward rewind incr :idx -1
:object forward fastforward set :idx {% expr {[llength ${:comment_block}] - 1} }
:object method process {-context:optional block} {
set :comment_block $block
# the defaults
set :processed_section context
set :current_entity [self]
if {[info exists context]} {
set :current_entity $context
set :processed_section description
}
set :is_not_completed 1
${:processed_section} eval [list set :context [self]]
set is_first_iteration 1
set :idx 0
set failure ""
while {${:is_not_completed}} {
set line [:dequeue]
if {$is_first_iteration} {
${:processed_section} on_enter $line
set is_first_iteration 0
}
if {[catch {${:processed_section} transition $line} failure]} {
set :is_not_completed 0
#
# TODO: For now, the fast-forward mechanism jumps to the end
# of the comment block; this avoids redundant on_exit
# calls. is there a better way of achieving this?
#
:fastforward
} else {
# NOTE: is_not_completed may be altered during transitions
set :is_not_completed [:has_next]
}
}
if {!$is_first_iteration} {
${:processed_section} on_exit $line
}
if {$failure ne ""} {
error $failure
}
return ${:current_entity}
}
}
#
# Infrastructure for state objects:
#
# 1. CommentState: a base class for sharing behaviour between atomic
# and non-orthogonal super-states; it is widely an intermediate,
# abstracted class, providing a refinement protocol for concrete
# state subclasses
#
Class create CommentState {
:attribute context; # points to the context object, i.e., an entity
:method on_enter {line} {;}
:method on_exit {line} {
#puts -nonewline stderr "EXIT -> [namespace tail [:info class]]#[namespace tail [self]]"
}
:method signal {event line} {;}
#
# activity/event interface
#
:method event=process {line} {;}
:method event=close {line} {;}
:method event=next {line} {;}
:method event=exit {msg} {
error $msg
}
:method event=rewind {line} {;}
}
# 2. CommentLines represent atomic states in the parsing state
# machinery: tag, text, space
Class create CommentLine -superclass CommentState {
:attribute comment_section; # points to the super-state objects
:attribute processed_line; # stores the processed text line
:forward signal {% ${:comment_section} } %proc
:forward context {% ${:comment_section} } %proc
:forward current_entity {% :context } eval set :current_entity
:method on_enter {line} {;}
:method on_exit {line} {;}
# :method event=next {line} {
# }
:method match {line} {;}
:method is? {line} {
foreach cline [lsort [[:info class] info instances]] {
if {[$cline match $line]} {
return [namespace tail $cline]
}
}
}
}
CommentLine create tag {
:method match {line} {
set tag [lindex $line 0]
return [expr {[string first @ $tag] == 0}]
}
:method event=process {line} {
set tag [lindex $line 0]
set entity [[:current_entity] {*}$line]
#puts stderr ENTITY=$entity,line=$line
# TODO: Fix the forward-setting of the current_entity. a) place
# it when exiting from the super-state? b) or, refactor it into the
# shadowed event=process method()? c) further options?
if {[::nx::core::is $entity object]} { :current_entity $entity }
}
}
CommentLine create text {
:method match {line} {
return [regexp -- {\s*[^[:space:]@]+} $line]
}
:method event=process {line} {
#
# TODO: revise when incremental support is operative
#
[:current_entity] doc add $line end
}
}
CommentLine create space {
:method match {line} {
return [expr {$line eq {}}]
}
}
#
# 3. CommentSections represent orthogonal super-states over
# CommentLines: context, description, part
#
Class create CommentSection -superclass CommentState {
:attribute entry_comment_line:required
:attribute current_comment_line
:attribute comment_line_transitions
:attribute next_comment_section; # implements a STATE-OWNED TRANSITION scheme
:method init {} {
${:entry_comment_line} comment_section [self]
}
:method transition {line} {
array set transitions ${:comment_line_transitions}
if {![info exists :current_comment_line]} {
set src ""
set tgt [${:entry_comment_line} is? $line]
} else {
set src ${:current_comment_line}
set tgt [$src is? $line]
}
#
# TODO: realise the initial state nodes as NULL OBJECTs, this
# helps avoid conditional branching all over the place!
#
if {$src ne ""} {
$src on_exit $line;
}
#puts stderr ${src}->${tgt}
# TODO: report invalid entry state explicitly
if {![info exists transitions(${src}->${tgt})]} {
error "Style violation in a [namespace tail [self]] section:
A $src line is followed by a $tgt line."
}
set :current_comment_line $tgt
$tgt comment_section [self]
${:current_comment_line} processed_line $line
${:current_comment_line} on_enter $line
foreach {event activities} $transitions(${src}->${tgt}) break;
:signal $event $line
foreach activity $activities {
:signal $activity $line
}
}
:method on_enter {line} {;}
:method on_exit {line} {
# TODO: move this behaviour into a more decent place
if {![${:context} has_next]} {
${:current_comment_line} on_exit $line
}
unset :current_comment_line
next;
}
:method signal {event line} {
${:current_comment_line} event=$event $line
:event=$event $line
}
#
# handled events
#
:method event=next {line} {
set next_section [:next_comment_section]
${:current_comment_line} on_exit $line
:on_exit $line
$next_section on_enter $line
$next_section eval [list set :context ${:context}]
${:context} eval [list set :processed_section [:next_comment_section]]
}
:method event=rewind {line} {
${:context} rewind
next
}
}; # CommentSection
#
# the OWNER-DRIVEN TRANSITIONS read as follows:
# (current_state)->(next_state) {event {activity1 activty2 ...}}
#
#
# context
#
CommentSection create context \
-next_comment_section description \
-comment_line_transitions {
->tag {process ""}
tag->space {process ""}
space->space {process ""}
space->text {close {rewind next}}
space->tag {close {rewind next}}
} -entry_comment_line tag
# NOTE: add these transitions for supporting multiple text lines for
# the context element
# tag->text {process ""}
# text->text {process ""}
# text->space {process ""}
#
# description
#
CommentSection create description \
-next_comment_section part \
-comment_line_transitions {
->text {process ""}
->tag {close {rewind next}}
text->text {process ""}
text->space {process ""}
space->space {process ""}
space->tag {close {rewind next}}
} -entry_comment_line text
#
# part
#
CommentSection create part \
-next_comment_section part \
-comment_line_transitions {
->tag {process ""}
tag->text {process ""}
text->text {process ""}
text->tag {close {rewind next}}
text->space {process ""}
space->space {process ""}
tag->space {process ""}
space->tag {close {rewind next}}
tag->tag {close {rewind next}}
} -entry_comment_line tag
}
puts stderr "Doc Tools loaded: [info command ::nx::doc]"