Index: library/lib/nxdoc-core.tcl =================================================================== diff -u -r6ef6700b363d0fcc6a4ccf78a9b51e27f5598936 -r187fbd20a453ae9d73e9b48f88b8d6a8c79685c2 --- library/lib/nxdoc-core.tcl (.../nxdoc-core.tcl) (revision 6ef6700b363d0fcc6a4ccf78a9b51e27f5598936) +++ library/lib/nxdoc-core.tcl (.../nxdoc-core.tcl) (revision 187fbd20a453ae9d73e9b48f88b8d6a8c79685c2) @@ -1,95 +1,50 @@ # @package nx::doc # -# Study for documentation classes for the Next Scripting Langauge +# The NXDoc infrastructure is built upon a representational model of +# NSF/NX code units; e.g., packages, commands, objects, and +# classes. This package declares the essential entities for +# representing NSF/NX programs, in terms of a special-purpose NSF/NX +# program. In addition, some utilities for implementing front- and +# backends for NXDoc are provided. # -# Compared to the "old" @ docmentation effort, this is a rather -# light-weight structure based on xotcl 2 (next) 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 issuing 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). -# +# @author stefan.sobernig@wu.ac.at # @require nx -# @version 0.1 +# @version 1.0 +# @namespace ::nx::doc package provide nx::doc 1.0 namespace eval ::nx::doc {} package require nx -package require nx::pp namespace eval ::nx::doc { namespace import -force ::nx::* # @command ::nx::doc::@ # - # The helper proc "@" is a conveniant way for creating new - # documentation objects with less syntactic overhead. + # The helper proc "@" is a conveniant mean for creating new + # documentation objects with minimal syntactic overhead. # - # @param class Request an instance of a particular entity class (e.g., ...) - # @param name What is the entity name (e.g., nx::doc for a package) - # @param args A vector of arbitrary arguments, provided to the + # @parameter class Request an instance of a particular entity class (e.g., ...) + # @parameter name What is the entity name (e.g., nx::doc for a package) + # @parameter args A vector of arbitrary arguments, provided to the # entity when being constructed # @return The identifier of the newly created entity object - # @subcommand ::nx::doc::@#foo - # - # This is the first subcommand foo of "@" - # {{{ - # set do 1; - # }}} - # - # @param -param1 do it - # @param param2 do it a second time - # @return Gives you a "foo" object - - # @subcommand ::nx::doc::@#bar - # - # This is the second subcommand bar of "@" - # - # @param -param1 do it - # @param param2 do it a second time - # @return Gives you a "bar" object - proc @ {class name args} {$class new -name $name {*}$args} # @command ::nx::doc::sorted # - # This proc is used to sort instances by values of a specified - # attribute. {{{ set - # code 1; puts stderr $code; puts stderr [info script]; set l \{x\} - # }}} Und nun gehen wir in eine zweite Zeile ... und fügen einen Link ein (e.g., {{@object ::nx::doc::@object}}) + # This utility proc is used to sort entities by values of a + # specified attribute. # - # ... um nach einem Zeilenbruch weiterzumachen - # {{{ - # \# Some comment - # set instances [list [Object new] [Object new]] - # ::nx::doc::sorted $instances; set l {{{x}}}; # Some comment - # {{{ }}} - # set instances [list [Object new] [Object new]] - # ::nx::doc::sorted $instances - # }}} - # Here it goes wider ... - # {{{ - # set instances [list [Object new] [Object new]] - # ::nx::doc::sorted $instances - # }}} - # - # @param instances Points to a list of entity instances to sort e.g. {{@object ::nx::doc::@object}} - # @param sortedBy Indicates the attribte name whose values the sorting will be based on - # @return A list of sorted documentation entity instances {{{instances of @object}}} + # @parameter instances Points to a list of entity instances + # to sort e.g. <<@class ::nx::doc::@object>> + # @parameter sortedBy Indicates the attribte name whose + # values the sorting will be based on + # @return A list of sorted documentation entity + # instances <<@class ::nx::doc::@object>> proc sorted {instances sortedBy} { set order [list] foreach v $instances {lappend order [list $v [$v eval [list set :$sortedBy]]]} @@ -109,15 +64,15 @@ } - proc sort_by_value {d} { + proc sortByValue {d} { set haystack [list] dict for {key value} $d { lappend haystack [list $key $value] } return [dict create {*}[concat {*}[lsort -integer -index 1 -decreasing $haystack]]] } - proc find_asset_path {{subdir library/lib/nxdoc-assets}} { + proc findAssetPath {{subdir library/lib/nxdoc-assets}} { # This helper tries to identify the file system path of the # asset ressources. # @@ -131,7 +86,7 @@ } - Class create MixinLayer { + Class create MixinLayer -superclass Class { :property {prefix ""} :public method init {} { set :active_mixins [dict create] @@ -187,8 +142,7 @@ namespace eval ::nx::doc::entities {} :public class method normalise {tagpath names} { - # puts stderr "tagpath $tagpath names $names" - # 1) verify balancedness of + # 1) verify balancedness of path spec elements if {[llength $tagpath] != [llength $names]} { return [list 1 "Imbalanced tag line spec: '$tagpath' vs. '$names'"] } @@ -231,7 +185,7 @@ # # TODO interp-aliasing objects under different command names # is currently not transparent to some ::nsf::* helpers, - # such as ::nsf::object::exists. Should this be changed? + # such as ::nsf::object::exists. Do we need to tackle this? # if {$cmd ne ""} { set cmd [namespace origin $cmd] @@ -253,7 +207,6 @@ if {[$entity info lookup methods -source application @$axis] eq ""} { return [list 1 "The tag '$axis' is not supported for the entity type '[namespace tail [$entity info class]]'"] } - #puts stderr "$entity @$axis id $value" set entity [$entity @$axis id $value] set last_axis $axis set last_name $value @@ -269,7 +222,7 @@ return [list 0 [expr {$all?$entity_path:$entity}]] } - # @method id + # @class.method {Tag id} # # A basic generator for the characteristic ideas, based on the # root_namespace, the tag label, and the fully qualified name of @@ -322,12 +275,12 @@ if {[::nsf::object::exists $id]} { $id configure {*}$args } else { - :create $id {*}$args + set id [:create $id {*}$args] } return $id } - # @method get_unqualified_name + # @class.method {Tag get_unqualified_name} # # @param qualified_name The fully qualified name (i.e., including the root namespace) :public method get_unqualified_name {qualified_name} { @@ -339,12 +292,47 @@ #return [string trimleft [string map [list ${:tag} ""] [:get_unqualified_name $qualified_name]] ":"] return [join [lrange [concat {*}[split [:get_unqualified_name $qualified_name] "::"]] 1 end] "::"] } + + # / / / / / / / / / / / / / / / / / / / / / / / / + # Manage chains of responsible container entities + # + # TODO: We don't need the stack-like dispensing of containers, + # make it a simple one-element store + + :public class property containers:0..*,object,type=::nx::doc::ContainerEntity { + set :incremental 1 + } + + :public method "containers empty" {} -returns boolean { + return [[current class] eval {expr {![info exists :containers] || ![llength ${:containers}]}}] + } + + :public method "containers peek" {} { + if {![:containers empty]} { + return [lindex [[current class] containers] end] + } + } + + :public method "containers push" {container:object,type=::nx::doc::ContainerEntity} { + set prev [:containers peek] + if {$prev ne ""} { + $container previous $prev + } + [current class] containers add $container end + } + + :public method "containers reset" {{v ""}} { + [current class] containers $v + } + } Class create QualifierTag -superclass Tag { :method get_fully_qualified_name {name} { if {![string match "::*" $name]} { - error "You need to provide a fully-qualified (absolute) entity name for '$name'." + set container [:containers peek] + set ns [$container getAuthoritativeNS] + set name ${ns}::$name } return $name } @@ -355,12 +343,9 @@ name } { if {[info exists partof_name]} { - #puts stderr "QUALIFIER=[join [list $partof_name $name] ::]" - #next [join [list $partof_name $name] ::] next } else { set n [:get_fully_qualified_name $name] -# puts stderr FINALNAME=$n next $n } } @@ -373,7 +358,6 @@ } { set id_name $name if {[info exists partof]} { - #set name [join [list [$partof name] $name] ::] set id_name ::[join [list [[$partof info class] get_tail_name $partof] $name] ::] } else { set name [:get_fully_qualified_name $name] @@ -400,7 +384,7 @@ } } - # @object ::nx::doc::PartAttribute + # @class ::nx::doc::PartAttribute # # This special-purpose Attribute variant realises (1) a cumulative # value management and (2) support for distinguishing between @@ -415,13 +399,20 @@ # part_class is given, the values will be transformed accordingly # before being pushed into the internal storage. - ::nx::MetaSlot create PartAttribute -superclass ::nx::VariableSlot { - - # @param part_class + nx::MetaSlot create PartAttribute -superclass ::nx::VariableSlot { + # @.parameter part_class # # The property refers to a concrete subclass of Part which # describes the parts being managed by the property. - :property part_class:optional,class + :property part_class:optional,class { + :public method assign {domain prop value} { + set owningClass [[$domain info parent] info parent] + if {"::nx::doc::ContainerEntity" in [concat $owningClass [$owningClass info heritage]]} { + $value class mixin add ::nx::doc::ContainerEntity::Containable + } + next + } + } :property scope :property {pretty_name {[string totitle [string trimleft [namespace tail [current]] @]]}} @@ -506,48 +497,57 @@ } } - Class create Entity { + # + # Sketch the entire hierarchy of documentation entities + # supported. Entity behaviour is defined further below + # + + Class create Entity + Class create StructuredEntity -superclass Entity + Class create ContainerEntity -superclass StructuredEntity + Class create PartEntity -superclass Entity + + Tag create @glossary -superclass Entity + Tag create @project -superclass ContainerEntity + Tag create @package -superclass ContainerEntity + QualifierTag create @command -superclass StructuredEntity + QualifierTag create @object -superclass StructuredEntity + QualifierTag create @class -superclass @object + + PartTag create @method -superclass StructuredEntity + PartTag create @param -superclass PartEntity + + + + Entity eval { # # Entity is the base class for the documentation classes # - # @param name + # @.parameter name # # gives you the name (i.e., the Nx object identifier) of the documented entity :property name:any,required - # every Entity must be created with a "@doc" value and can have - # an optional initcmd - #:method objectparameter args { - #next [list [list @doc:optional __initcmd:initcmd,optional]] - #} - :class property current_project:object,type=::nx::doc::@project,0..1 :public forward current_project [current] %method :property partof:object,type=::nx::doc::StructuredEntity :property part_attribute:object,type=::nx::doc::PartAttribute - + + + :public method get_fqn_command_name {} { + return ${:name} + } + # # TODO: the pdata/pinfo/validate combo only makes sense for # entities which reflect Tcl program structures -> refactor into a # dedicated PEntity class or the like # - - :public method get_fqn_command_name {} { - return ${:name} - } - + :property pdata - :public method validate {} { - if {[info exists :pdata] && \ - [:pinfo get -default complete status] ne "missing"} { - if {[[:origin] as_list] eq ""} { - :pinfo propagate status mismatch - :pinfo lappend validation "Provide a short, summarising description!" - } - } - } + :public method "pinfo get" {{-default ?} args} { if {![info exists :pdata] || ![dict exists ${:pdata} {*}$args]} { return $default; @@ -618,7 +618,6 @@ set :default 0 } - # :property @properties -class ::nx::doc::PartAttribute :public method @property {props} { foreach prop $props { :@$prop @@ -627,24 +626,9 @@ :property @use { :public method assign {domain prop value} { - # @command nx - # - # @use ::nsf::command - - # or - - # class.method {X foo} - # - # @use {Class foo} - # @use object.method {Object foo} - lassign $value pathspec pathnames if {$pathnames eq ""} { set pathnames $pathspec - # puts stderr PATH=[$domain get_upward_path \ - # -attribute {[:info class] tag}] - # puts stderr "dict create {*}[$domain get_upward_path \ - # -attribute {[:info class] tag}]" set pathspec [dict create {*}[$domain get_upward_path \ -attribute {[:info class] tag}]] set pathspec [dict values $pathspec] @@ -657,22 +641,19 @@ } lassign $res pathspec pathnames - #puts stderr "PATHSPEC $pathspec PATHNAMES $pathnames" lassign [::nx::doc::Tag find $pathspec $pathnames] err res if {$err} { error "Generating an entity handle failed: $res" } - # puts stderr "NEXT $domain $prop $res" next [list $domain $prop $res] } } :public method origin {} { if {[info exists :@use]} { - # puts stderr ORIGIN(${:@use})=isobj-[::nsf::object::exists ${:@use}] if {![::nsf::object::exists ${:@use}] || ![${:@use} info has type [:info class]]} { - error "Referring to a non-existing doc entity or a doc entity of a different type." + return -code error "Referring to a non-existing doc entity or a doc entity of a different type." } return [${:@use} origin] } @@ -686,7 +667,7 @@ } } - # @method as_text + # @.method as_text # # text is used to access the content of doc of an Entity, and # performs substitution on it. The substitution is not essential, @@ -700,16 +681,23 @@ } return [subst [join $doc " "]] } + + :public method error {msg} { + return -code error "[current].[uplevel 1 [list ::nsf::current method]](): $msg" + } + } - Tag create @glossary -superclass Entity { + + # @class @glossary + @glossary eval { :property @pretty_name :property @pretty_plural :property @acronym } - Class create StructuredEntity -superclass Entity { + StructuredEntity eval { :public method part_attributes {} { set slots [:info lookup slots] @@ -753,145 +741,87 @@ } return $__owned_parts } - - :public method validate {} { - next - dict for {s entities} [:owned_parts -where "!\${:@stashed}"] { - foreach e $entities { - # TODO: for now, it is sufficient to escape @use chains - # here. review later ... - if {![$e eval {info exists :@use}]} { - $e [current method] - } - } - } - } + } - Class create ContainerEntity -superclass StructuredEntity { - - Class create [current]::Resolvable { - :class property container:object,type=[:info parent] - :method get_fully_qualified_name {name} { - set container [[current class] container] - if {![string match "::*" $name]} { -# puts -nonewline stderr "--- EXPANDING name $name" - set name [$container @namespace]::$name -# puts stderr " to name $name" - } - next $name - } - } + ContainerEntity eval { Class create [current]::Containable { - # TODO: check the interaction of required, per-object property and ::nsf::assertion - #:object property container:object,type=[:info parent],required - :property container:object,type=[:info parent] :method create args { - # - # Note: preserve the container currently set at this callstack - # level. [next] will cause the container to change if another - # container entity is initialised in the following! - # - if {[[current class] eval {info exists :container}]} { - set container [[current class] container] - set obj [next] - if {![$obj eval {info exists :partof}]} { - $container register $obj - } - return $obj - } else { - next + # + # Note: preserve the container currently set at this callstack + # level. [next] might cause another container to be pushed on + # top. + # + set cont [:containers peek] + set obj [next] + if {![$obj eval {info exists :partof}] && $cont ne ""} { + $cont register $obj } + return $obj } - :method create args { - # - # Note: preserve the container currently set at this callstack - # level. [next] will cause the container to change if another - # container entity is initialised in the following! - # - if {[info exists :container]} { - set cont ${:container} - set obj [next] - if {![$obj eval {info exists :partof}]} { - $cont register $obj - } - return $obj - } else { - next - } - } - } + # Note: The default "" corresponds to the top-level namespace "::"! :property {@namespace ""} - + :property -class ::nx::doc::PartAttribute @class { :pretty_name "Class" :pretty_plural "Classes" - set :part_class ::nx::doc::@class + :part_class ::nx::doc::@class } :property -class ::nx::doc::PartAttribute @object { :pretty_name "Object" :pretty_plural "Objects" - set :part_class ::nx::doc::@object + :part_class ::nx::doc::@object } :property -class ::nx::doc::PartAttribute @command { :pretty_name "Command" :pretty_plural "Commands" - set :part_class ::nx::doc::@command + :part_class ::nx::doc::@command } - # :property @class:object,type=::nx::doc::@class,multivalued { - # set :incremental 1 - # } + :public method register {containable:object,type=::nx::doc::Entity} { + set tag [[$containable info class] tag] + if {[:info lookup methods -source application "@$tag"] ne ""} { + :@$tag $containable + } elseif {[info exists :previous]} { + ${:previous} register $containable + } + } - # :property @object:object,type=::nx::doc::@object,multivalued { - # set :incremental 1 - # } + :property previous:object,type=[current] - # :property @command:object,type=::nx::doc::@command,multivalued { - # set :incremental 1 - # } + :public method announceAsContainer {tag:object,type=::nx::doc::Tag} { + $tag containers push [current] + } - # :method init {} { - # next - - # QualifierTag mixin add [current class]::Resolvable - # [current class]::Resolvable container [current] - # foreach {attr part_class} [:part_attributes] { - # $part_class class mixin add [current class]::Containable - # $part_class container [current] - # } - # } - - :method destroy {} { - foreach {attr part_class} [:part_attributes] { - #$part_class class mixin add [current class]::Containable - if {[$part_class eval {info exists :container}] && \ - [$part_class container] eq [current]} { - $part_class eval {unset :container} - } + :public method getAuthoritativeNS {} { + if {${:@namespace} eq "" && [info exists :previous]} { + return ${:previous} [current method] + } else { + return ${:@namespace}; # defaults to top-level/global NS } - next } - :public method register {containable:object,type=::nx::doc::Entity} { - set tag [[$containable info class] tag] - if {[:info lookup methods -source application "@$tag"] ne ""} { - :@$tag $containable - } + :protected method init args { + next + :announceAsContainer [:info class] } + } - Tag create @project -superclass ContainerEntity { + @project eval { - :property sandbox:object,type=::nx::doc::Sandbox - :property sources + # / / / / / / / / / / / / / / / / / / + # Doc entity interface + # / / / / / / / / / / / / / / / / / / + :public property sandbox:object,type=::nx::doc::Sandbox + :property url :property license :property creationdate @@ -901,7 +831,7 @@ :property depends:0..*,object,type=[current] :property -class ::nx::doc::PartAttribute @glossary { - set :part_class ::nx::doc::@glossary + :part_class ::nx::doc::@glossary :public method get {domain prop} { set l [next] if {[$domain eval {info exists :depends}]} { @@ -916,9 +846,167 @@ :property -class ::nx::doc::PartAttribute @package { :pretty_name "Package" :pretty_plural "Packages" - set :part_class ::nx::doc::@package + :part_class ::nx::doc::@package } + # / / / / / / / / / / / / / / / / / / + # Frontend interface + # / / / / / / / / / / / / / / / / / / + + :private method "frontend unknown" {m args} { + :error "The NXDoc frontend '$m' is not available." + } + + :public method read {frontend srcs cmds} -returns 0..*,object,type=::nx::doc::Entity { + :frontend $frontend $srcs $cmds + } + + :public class method newFromSources { + {-frontend dc} + {-sandboxed:boolean 1} + -include + -exclude + sources + args + } { + + # + # Action 1) Object creation + # + set newPrj [:new {*}$args] + + # + # Action 2) Initialise a sandbox + # + set sandbox [$newPrj sandbox [Sandbox new -interp [expr {$sandboxed?[interp create]:""}]]] + + # + # Action 3) Extract documentation sources (1pass) + # + $sandbox do [$newPrj get1PassScript $sources] + set sourceScripts [$sandbox getDocumentationScripts] + + # + # Action 4) Determine command population through introspection (2pass) + # + $sandbox do [$newPrj get2PassScript $sourceScripts] + + + # + # Action 5) Applying command filters and obtain the workspace in + # terms of commands ... + # + if {[info exists include] && [info exists exclude]} { + $newPrj error "Inclusion and exclusion constraints are mutually exclusive!" + } + + set nsFilters [list] + if {[info exists include] && $include ne ""} { + set nsFilters [list $include] + } + if {[info exists exclude] && $exclude ne ""} { + set nsFilters [list -not $exclude] + } + + set commandsFound [$sandbox getCommandsFound {*}$nsFilters] + + # + # Action 6) Load the requested frontend extension + # + package req nx::doc::$frontend + + # + # Action 7) Have the intended documentation entities processed + # (documented, and meant to be visible) + # + # $newPrj readSrcs $frontend $sourceScripts $commandsFound + $newPrj read $frontend $sourceScripts $commandsFound + + return $newPrj + } + + :public method get1PassScript {sources} { + set 1pass "::nx::doc::__trace_pkg\n" + dict for {srcType items} $sources { + if {![llength $items]} continue; + switch -exact -- $srcType { + package { + foreach i $items { + append 1pass "package require $i\n" + } + } + source { + foreach i $items { + append 1pass "source $i\n" + } + } + eval { + error "Not implemented!" + # foreach i $items { + # append 1pass "info script X-EVAL\n" + # append 1pass "$i\n" + # } + # set srcType source + # set items X-EVAL + } + default { + error "Unsupported documentation source type '$srcType'" + } + } + ${:sandbox} permissive lappend $srcType $items + } + return $1pass + } + + :public method get2PassScript {sourceScripts} { + set 2pass "::nx::doc::__init\n" + dict for {id info} $sourceScripts { + set block "%s" + dict with info { + # Available vars: + # + # package + # path + # script + # dependency + # + if {$dependency || ![info exists script]} continue; + + if {[info exists package]} { + set fragment " + ::nx::doc::__cpackage push $package; + %s + ::nx::doc::__cpackage pop; + " + set block [format $block $fragment] + unset package + } + + if {[info exists path]} { + set block [format $block "info script $path;\n%s"] + unset path + } + } + append 2pass [format $block $script] + unset script + } + return $2pass + } + + # / / / / / / / / / / / / / / / / / / + # Backend interface + # / / / / / / / / / / / / / / / / / / + + :public method write {{-format html} args} { + package req nx::doc::$format + $format run -project [current] {*}$args + } + + + # / / / / / / / / / / / / / / / / / / + # Lifecycling + # / / / / / / / / / / / / / / / / / / + :public method destroy {} { # # TODO: Using the auto-cleanup feature in [Test case ...] does @@ -936,46 +1024,37 @@ :method init {} { # # TODO: the way we provide the project as a context object to - # all entities is not easily restricted. Review later ... - # - :current_project [current]; # sets a per-class-object variable on Entity! + # all entities is not easily restricted. Review later (e.g., + # relocate into the Validator) ... + # + [current class] containers reset + :current_project [current]; # side effect: sets a per-class-object variable on Entity! next } + } - # - # Now, define some kinds of documentation entities. The toplevel - # docEntities are named objects in the ::nx::doc::entities namespace - # to ease access to it. - # - # For now, we define here the following toplevel docEntities: - # - # - @package - # - @command - # - @object - # - ... - # - # These can contain multiple parts. - # - @method - # - @param - # - ... - # + + # TODO: decide how to deal with @package and @project names (don't + # need namespace delimiters!) - Tag create @package -superclass ContainerEntity { + @package eval { :property -class ::nx::doc::PartAttribute @require - :property -class ::nx::doc::PartAttribute @version + :property @version + :property -class ::nx::doc::PartAttribute @author + } - QualifierTag create @command -superclass StructuredEntity { + @command eval { :property -class ::nx::doc::PartAttribute @parameter { - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :property -class ::nx::doc::PartAttribute @return { :method require_part {domain prop value} { set value [expr {![string match ":*" $value] ? "__out__: $value": "__out__$value"}] next [list $domain $prop $value] } - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :public forward @sub-command %self @command @@ -984,103 +1063,48 @@ :pretty_name "Subcommand" :pretty_plural "Subcommands" :public method id {domain prop value} { - # TODO: [${:part_class}] resolves to the property slot - # object, not the global @command object. is this intended, in - # line with the intended semantics? return [${:part_class} [current method] \ -partof_name [$domain name] \ -scope ${:scope} -- $value] } - set :part_class ::nx::doc::@command + :part_class ::nx::doc::@command } - - :public method validate {} { - if {[info exists :pdata] && \ - [:pinfo get -default complete status] ne "missing"} { - - if {![info exists :@command]} { - set params [list] - set param_names [list] - if {[info exists :@parameter]} { - foreach p [:@parameter] { - set value [$p name] - lappend param_names $value - if {[$p eval {info exists :default}] || $value eq "args" } { - set value "?$value?" - } - lappend params $value - } - } - - set ps [:pinfo get -default "" bundle parameter] - dict for {actualparam paraminfo} $ps { - if {$actualparam ni $param_names} { - set p [:@parameter $actualparam] - $p pdata [lappend paraminfo status missing] - } - } - } - - if {![:pinfo exists bundle parametersyntax]} { - :pinfo set bundle parametersyntax $params - } - - # Note: [next] will cause the missing parameter created to - # be validated and will have the appropriate status - # propagated upstream! - next - } - } } - QualifierTag create @object \ - -superclass StructuredEntity \ - -mixin ContainerEntity::Containable { + @object eval { + + :public forward @object %self @child-object + + :property -class ::nx::doc::PartAttribute @child-object { + :part_class ::nx::doc::@object + :public method id {domain prop value} { + return [${:part_class} id [join [list [$domain name] $value] ::]] + } + + } + + :public forward @class %self @child-class + + :property -class ::nx::doc::PartAttribute @child-class { + :part_class ::nx::doc::@class + :public method id {domain prop value} { + return [${:part_class} id [join [list [$domain name] $value] ::]] + } + } - :property -class ::nx::doc::PartAttribute @author - - :public forward @object %self @child-object - - :property -class ::nx::doc::PartAttribute @child-object { - set :part_class ::nx::doc::@object - :public method id {domain prop value} { -# puts stderr "CHILD-OBJECT: [current args]" - # if {![info exists :part_class]} { - # error "Requested id generation from a simple part property!" - # } - return [${:part_class} id [join [list [$domain name] $value] ::]] -# return [${:part_class} id -partof_name [$domain name] -scope ${:scope} $value] - } - - } - - :public forward @class %self @child-class - - :property -class ::nx::doc::PartAttribute @child-class { - set :part_class ::nx::doc::@class - :public method id {domain prop value} { - #puts stderr "CHILD-CLASS: [current args]" - # if {![info exists :part_class]} { - # error "Requested id generation from a simple part property!" - # } - return [${:part_class} id [join [list [$domain name] $value] ::]] - #return [${:part_class} id -partof_name [$domain name] -scope ${:scope} $value] - } - } - :public forward @method %self @object-method :property -class ::nx::doc::PartAttribute @object-method { :pretty_name "Object method" :pretty_plural "Object methods" - set :part_class ::nx::doc::@method + :part_class ::nx::doc::@method } :public forward @property %self @object-property #:forward @param %self @object-param :property -class ::nx::doc::PartAttribute @object-property { - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :method undocumented {} { @@ -1097,8 +1121,7 @@ } } - QualifierTag create @class \ - -superclass @object { + @class eval { :property -class ::nx::doc::PartAttribute @superclass @@ -1107,7 +1130,7 @@ :property -class ::nx::doc::PartAttribute @class-property { :pretty_name "Per-class attribute" :pretty_plural "Per-class attributes" - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :public forward @class-object-method %self @object-method @@ -1118,15 +1141,15 @@ :property -class ::nx::doc::PartAttribute @class-hook { :pretty_name "Hook method" :pretty_plural "Hook methods" - set :part_class ::nx::doc::@method + :part_class ::nx::doc::@method } :public forward @method %self @class-method :property -class ::nx::doc::PartAttribute @class-method { :pretty_name "Provided method" :pretty_plural "Provided methods" - set :part_class ::nx::doc::@method + :part_class ::nx::doc::@method :method require_part {domain prop value} { # TODO: verify whether these scoping checks are sufficient # and/or generalisable: For instance, is the scope @@ -1142,50 +1165,29 @@ next } } - :public method validate {} { - next - # - # TODO: Certain metadata could also be valid in "missing" - # state, e.g., paramtersyntax? Re-arrange later ... - # - if {[info exists :pdata] && - [:pinfo get -default complete status] ne "missing"} { - # - # Note: Some metadata on classes cannot be retrieved from - # within the tracers, as they might not be set local to the - # class definition. Hence, we gather them at this point. - # - set prj [:current_project] - set box [$prj sandbox] - set statement [list ::nsf::dispatch ${:name} \ - ::nsf::methods::class::info::objectparameter \ - parametersyntax] - :pinfo set bundle parametersyntax [$box eval $statement] - } - } } - Class create PartEntity -superclass Entity { + + PartEntity eval { :property partof:object,type=::nx::doc::StructuredEntity,required :property part_attribute:object,type=::nx::doc::PartAttribute,required } - # @object ::nx::doc::@method + # @class ::nx::doc::@method # # "@method" 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. # - PartTag create @method \ - -superclass StructuredEntity { + @method eval { :property -class ::nx::doc::SwitchAttribute @syshook:boolean { set :default 0 } :property -class ::nx::doc::PartAttribute @parameter { - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :property -class ::nx::doc::PartAttribute @return { # @@ -1200,7 +1202,7 @@ set value [expr {![string match ":*" $value] ? "__out__: $value": "__out__$value"}] next [list $domain $prop $value] } - set :part_class ::nx::doc::@param + :part_class ::nx::doc::@param } :public class method new { @@ -1224,7 +1226,7 @@ :public forward @sub-method %self @method :property -class ::nx::doc::PartAttribute @method { - set :part_class ::nx::doc::@method + :part_class ::nx::doc::@method :public method id {domain prop name} { # TODO: ${:part_class} resolves to the local slot # [current], rather than ::nx::doc::@method. Why? @@ -1242,62 +1244,6 @@ return ::nsf::${scope}::[string trimleft [[:partof] name] :]::${:name} } - # @method->validate() - :public method validate {} { - set partof [:get_owning_partof] - if {[info exists :pdata] && - [:pinfo get -default complete status] ne "missing"} { - # - # Note: Some information on methods cannot be retrieved from - # within the tracers as they might not be set local to the - # method definition. Hence, we gather them at this point. I - # will review whether there is a more appropriate way of - # dealing with this issue ... - # - set prj [:current_project] - set box [$prj sandbox] - set obj [$partof name] - - if {[:pinfo exists bundle handle]} { - set handle [:pinfo get bundle handle] - :pinfo set bundle redefine-protected [$box eval [list ::nsf::method::property $obj $handle redefine-protected]] - :pinfo set bundle call-protected [$box eval [list ::nsf::method::property $obj $handle call-protected]] - } - - set params [list] - set param_names [list] - if {[info exists :@parameter]} { - foreach p [:@parameter] { - set value [$p name] - lappend param_names $value - if {[$p eval {info exists :default}] || $value eq "args" } { - set value "?$value?" - } - lappend params $value - } - } - - dict for {actualparam paraminfo} [:pinfo get -default "" bundle parameter] { - if {$actualparam ni $param_names} { - set p [:@parameter $actualparam] - $p pdata [lappend paraminfo status missing] - } - } - - if {![:pinfo exists bundle parametersyntax]} { - :pinfo set bundle parametersyntax $params - } - - # Note: [next] will cause the missing parameter created to - # be validated and will have the appropriate status - # upstream! - next - } else { - # To realise upward status propagation for submethods, use: - # ${:partof} pinfo propagate status mismatch - $partof pinfo propagate status mismatch - } - } :public method get_sub_methods {} { if {[info exists :@method]} { @@ -1337,36 +1283,20 @@ }; # @method - # PartTag create @subcommand -superclass {Part @command} - # PartTag create @subcommand -superclass {Part @command} - - # @object ::nx::doc::@param + # @class ::nx::doc::@param # # The entity type "@param" represents the documentation unit # for several parameter types, e.g., object, method, and # command parameters. # - PartTag create @param \ - -superclass PartEntity { - - #:property spec + @param eval { :property -class ::nx::doc::PartAttribute @spec :property default :public class method id {partof_name scope name} { next [list [:get_unqualified_name ${partof_name}] $scope $name] } - - # :class method id {partof_name name} { - # # The method contains the parameter-specific name production rules. - # # - # # @param partof Refers to the entity object which contains this part - # # @param name Stores the name of the documented parameter - - # set partof_fragment [:get_unqualified_name ${partof_name}] - # return [:root_namespace]::${:tag}::${partof_fragment}::${name} - # } - + # @class-object-method new # # The per-object method refinement indirects entity creation @@ -1403,46 +1333,6 @@ next } } - - - # @param->validate() - :public method validate {} { - # - # TODO: For now, we escape from @param validaton on command - # parameters. There is no equivalent to [info parameter] - # available, so we would need to cook a substitute based on - # the parametersyntax. Review later ... - # - if {${:name} eq "__out__" && \ - [${:partof} info has type ::nx::doc::@command]} return; - - # - # Here, we escape from any parameter verification for - # parameters on forwards & alias, as there is no basis for - # comparison! - # - if {[${:partof} info has type ::nx::doc::@method] && \ - [${:partof} pinfo get bundle type] in [list forward alias]} { - dict set :pdata status "" - return; - } - - if {[info exists :pdata] && \ - [:pinfo get -default complete status] ne "missing"} { - - # valid for both object and method parameters - set pspec [:pinfo get -default "" bundle spec] - if {[info exists :spec] && \ - ${:spec} ne $pspec} { - :pinfo propagate status mismatch - :pinfo lappend validation "Specification mismatch. Expected: \ - '${:spec}' Got: '$pspec'." - } - next - } else { - ${:partof} pinfo propagate status mismatch - } - } } # @@ -1667,7 +1557,6 @@ return $line } - :method as_list {} { set preprocessed [list] set is_code_block 0 @@ -1677,11 +1566,8 @@ set is_code_block [expr {!$is_code_block}] append line \n } elseif {${is_code_block}} { - # set line [:map $line unescape] append line \n } else { - # set line [:map $line sub] - # set line [:map $line unescape] set line [string trimleft $line] if {$line eq {}} { set line "\n\n" @@ -1696,6 +1582,7 @@ set preprocessed [join [:as_list] " "] set preprocessed [:map $preprocessed] set preprocessed [:unescape $preprocessed] + # TODO: For now, we take a passive approach: Some docstrings # might fail because they contain substitution characters # ($,[]); see nx.tcl. The same goes for legacy xodoc docstrings, @@ -1704,6 +1591,7 @@ # escape/unescape evaluation chars; at the same time, we can't # distinguish errors on unintended and intended evaluations. # ... + if {[catch {set preprocessed [subst $preprocessed]} msg]} { puts stderr SELF=[current] puts stderr MSG=$msg @@ -1716,6 +1604,7 @@ } # + # NXDoc backend infrastructure: # A Renderer base class ... # Class create Renderer -superclass MixinLayer { @@ -1791,7 +1680,7 @@ } :method readAsset {assetName} { - set assetDir [find_asset_path] + set assetDir [findAssetPath] set assetPath [file join $assetDir $assetName] return [:read $assetPath] } @@ -1909,9 +1798,6 @@ {-theme yuidoc} args } { - # TODO: Relocate trigger validation! - $project validate - # -- :apply :current_theme $theme :layout $layout $project $theme {*}$args @@ -1966,49 +1852,84 @@ return $value } - :protected property {current_packages "*"} - :property {permissive_pkgs:1..* "*"} { - set :incremental 1 + :protected property {current_packages ""} + + :public method "permissive lappend" {type value} { + set d [lindex [current methodpath] 0] + dict [current method] :$d $type {*}$value } + :public method "permissive get" {type} { + if {![info exists :permissive]} { + set :permissive [dict create] + } + dict [current method] ${:permissive} $type + } + + :public method getDocumentationScripts {} { + if {[info exists :dSources]} { + return ${:dSources} + } + } + # # some callbacks invoked from within the sandbox interp # - :public method "cpackage pop" {} { set :current_packages [lrange ${:current_packages} 0 end-1] } :public method "cpackage push" {p} { - lappend :current_packages [string tolower $p] + lappend :current_packages $p } :public method "cpackage top" {} { - return [lindex ${:current_packages} end] + if {[info exists :current_packages]} { + return [lindex ${:current_packages} end] + } } - :public method at_source {filepath} { + :public method at_source {filePath} { set cpackage [:cpackage top] - if {$cpackage in ${:permissive_pkgs}} { - lappend :source $cpackage $filepath + set fh [open $filePath r] + set script [read $fh] + catch {close $fh} + + set info [dict create] + set key "" + if {$cpackage ne ""} { + set key "$cpackage." + dict set info package $cpackage + dict set info dependency [expr {$cpackage ni [:permissive get package]}] } else { - dict set :deps $filepath $cpackage + # TODO: dict set info dependency [expr {$filePath ni [:permissive get source]}] + dict set info dependency 1 } + dict set info path $filePath + dict set info script $script + + dict set :dSources $key$filePath $info } :public method at_load {filepath} { set cpackage [:cpackage top] - if {$cpackage ne ${:permissive_pkgs}} { - dict set :deps $filepath $cpackage + set key "" + set info [dict create] + dict set info path $filepath + if {$cpackage ne ""} { + set key "$cpackage." + dict set info package $cpackage + dict set info dependency [expr {$cpackage ni [:permissive get package]}] + } else { + # TODO: dict set info dependency [expr {$filePath ni [:permissive get source]}] + dict set info dependency 1 } - } + dict set :dSources $key$filepath $info + } :public method at_register_package {pkg_name version} { dict set :registered_packages $pkg_name version $version } -# :public method at_deregister_package {} { -# set :current_packages [lrange ${:current_packages} 0 end-1] -# } - # [list ->status:in,arg=complete|missing|prototype|mismatch,slot=[current] missing] + :public method at_register_command [list \ name:fqn,slot=[current] \ ->cmdtype:in,arg=@object|@class|@command|@method,slot=[current] \ @@ -2018,32 +1939,31 @@ ->docstring:optional,slot=[current] \ ->bundle ] { - # peek the currently processed package (if any) set storable_vars [info vars >*] - # set cpackage [lindex ${:current_packages} end] + foreach svar $storable_vars { + dict set :registered_commands $name [string trimleft $svar >] [set $svar] + } + + # peek the currently processed package (if any) set cpackage [:cpackage top] - if {$cpackage in ${:permissive_pkgs}} { + if {$cpackage ne ""} { dict set :registered_commands $name package $cpackage - foreach svar $storable_vars { - dict set :registered_commands $name [string trimleft $svar >] [set $svar] - } + dict set :registered_commands $name dependency \ + [expr {$cpackage ni [:permissive get package]}] + } else { + # FIXME dict set :registered_commands $name dependency \ + # [expr {$source ni [:permissive get source]}] + dict set :registered_commands $name dependency 1 } } :public method at_deregister_command [list name:fqn,slot=[current]] { - set cpackage [:cpackage top] - if {$cpackage in ${:permissive_pkgs}} { - dict unset :registered_commands $name - } + dict unset :registered_commands $name } :public method init args { :do { - # - # hide selected built-in Tcl commands and put simple - # forwarding proxies in place ... - # # TODO: refactor the proxy handling ... # interp hide "" proc @@ -2054,14 +1974,13 @@ interp hide "" auto_import interp invokehidden "" proc ::proc args { - #set ns [uplevel [list interp invokehidden "" namespace current]] uplevel [list interp invokehidden "" proc {*}$args] } proc ::namespace args { - #set ns [uplevel [list interp invokehidden "" namespace current]] - #interp invokehidden "" -namespace $ns namespace {*}$args + # set ns [uplevel [list interp invokehidden "" namespace current]] uplevel [list interp invokehidden "" namespace {*}$args] + # interp invokehidden {} -namespace $ns namespace {*}$args } proc ::source args { @@ -2131,7 +2050,6 @@ proc list_commands {{parent ""}} { set ns [dict create] - #set cmds [string trim "[join [info commands ${parent}::*] \" 0 \"] 0" 0] # # Note: We trigger a [namespace import] for the # currently processed namespace before requesting the @@ -2140,14 +2058,12 @@ # i.e. after having computed the [info # commands] snapshot! # -# namespace eval ::nx::doc::__x [list namespace import -force ${parent}::*] + set cmds [info commands ${parent}::*] set exported [list] foreach cmd $cmds { dict set ns ::[string trimleft $parent :] $cmd [is_exported $cmd] - -#[expr {[info commands ::nx::doc::__x::[namespace tail $cmd]] ne ""}] } foreach nsp [namespace children ${parent}::] { @@ -2164,8 +2080,6 @@ # # pre-state # - # set pre_loaded [dict values \ - # [dict create {*}[concat {*}[info loaded ""]]]] set pre_loaded [lreverse [concat {*}[info loaded ""]]] set pre [::nx::doc::list_commands] set pre_commands [dict create {*}[concat {*}[dict values $pre]]] @@ -2176,7 +2090,6 @@ # # post-state # - #set post_loaded [dict create {*}[concat {*}[info loaded ""]]] set post_loaded [lreverse [concat {*}[info loaded ""]]] set post [::nx::doc::list_commands] set post_commands [dict create {*}[concat {*}[dict values $post]]] @@ -2192,10 +2105,6 @@ set delta_pkg [dict remove \ [dict create {*}$post_loaded] \ [dict keys [dict create {*}$pre_loaded]]] - - #puts stderr "DELTAS pkg $delta_pkg" - #puts stderr "DELTAS namespace $delta_namespaces" - #puts stderr "DELTAS commands $delta_commands" lassign $delta_pkg pkg_name filepath set filepath [file normalize $filepath] @@ -2207,8 +2116,9 @@ # parametersyntax prints. if {[info commands ::nsf::objectsystem::create] ne "" && \ [::nsf::configure objectsystem] eq ""} { - set rootclass ::nx::doc::_%&obj - set rootmclass ::nx::doc::_%&cls + namespace eval ::nx::doc::_%& {} + set rootclass ::nx::doc::_%&::obj + set rootmclass ::nx::doc::_%&::cls ::nsf::objectsystem::create $rootclass $rootmclass } else { lassign {*}[::nsf::configure objectsystem] rootclass rootmclass @@ -2268,10 +2178,6 @@ } } interp invokehidden "" -namespace $ns package $subcmd {*}$args - # uplevel [list interp invokehidden "" package $subcmd {*}$args] -# if {$was_registered} { -# ::nx::doc::__at_deregister_package -# } } # @@ -2303,7 +2209,7 @@ # helper objsys to retrieve command parameter specs and # parametersyntax prints. # - if {$rootclass ne "::nx::doc::_%&obj"} { + if {$rootclass ne "::nx::doc::_%&::obj"} { ::nsf::configure keepinitcmd true; @@ -2333,7 +2239,6 @@ {*}[expr {[::nsf::var::exists $obj __initcmd] && [::nsf::var::set $obj __initcmd] ne ""?[list ->docstring [::nsf::var::set $obj __initcmd]]:[list]}] return $obj } - # ::nsf::relation $rootmclass class-mixin ${::nx::doc::rootns}::__Tracer if {[info commands "::nx::Object"] ne ""} { $rootmclass $sysmeths(-class.create) ${::nx::doc::rootns}::__ObjTracer @@ -2481,35 +2386,6 @@ return $handle } - # if {[$object info method type ${:name}] eq "forward"} { - # set cmd "" - # foreach w [lrange [$object info method definition ${:name}] 2 end] { - # if {[string match ::* $w]} { - # set cmd $w - # break - # } - # } - # if {$cmd ne "" && [string match ::nsf::* $cmd]} { - # # TODO: we assume here, the cmd is a primitive - # # command and we intend only to handle cases from - # # predefined or xotcl2. Make sure this is working - # # reasonable for other cases, such as forwards to - # # other objects, as well - # if {![catch {set actualParams [::nx::Object info method parameter $cmd]}]} { - # # drop usual object - # set actualParams [lrange $actualParams 1 end] - # # drop per object ; TODO: always? - # if {[lindex $actualParams 0] eq "-per-object"} { - # set actualParams [lrange $actualParams 1 end] - # set syntax [lrange [::nx::Object info method parametersyntax $cmd] 2 end] - # } else { - # set syntax [lrange [::nx::Object info method parametersyntax $cmd] 1 end] - # } - # } - # } - # } - - rename ::nsf::method::forward ::nsf::_%&forward ::interp invokehidden "" proc ::nsf::method::forward { args @@ -2660,28 +2536,6 @@ :public property registered_commands - :public method getCompanions {identifiers} { - set scripts [list] - dict for {source pkg} $identifiers { - set rootname [file rootname $source] - set dir [file dirname $source] - set companion $rootname.nxd - set srcs [dict create {*}"[join [list $source $rootname.nxd [file join $dir $pkg].nxd] " _ "] _"] - foreach src [dict keys $srcs] { - if {![file isfile $src] || ![file readable $src]} continue; - if {[file extension $src] eq [info sharedlibextension]} continue; - set fh [open $src r] - if {[catch {lappend scripts [read $fh]} msg]} { - catch {close $fh} - :log "error reading the file '$thing', i.e.: '$msg'" - } - catch {close $fh} - } - } - return $scripts - - } - :public method get_companions {} { set companions [dict create] dict for {cmd props} ${:registered_commands} { @@ -2693,8 +2547,9 @@ return [:getCompanions $companions] } - :public method get_registered_commands { - -exported:switch + :public method getCommandsFound { + -exported:boolean + -imported:switch -types -not:switch nspatterns:optional @@ -2709,31 +2564,26 @@ dict filter ${:registered_commands} script {cmd props} { dict with props { expr {[expr {[info exists nspatterns]?[expr {[regexp -- $nspatterns $cmd _] != $not}]:1}] && \ - [expr {$exported?[expr {$nsexported == $exported}]:1}] && \ - [expr {[info exists types]?[expr {$cmdtype in $types}]:1}]} + [expr {[info exists exported]?[expr {$nsexported == $exported}]:1}] && \ + [expr {$nsimported == $imported}] && \ + [expr {[info exists types]?[expr {$cmdtype in $types}]:1}]} } } - #lsearch -inline -all -regexp $additions {^::nsf::[^\:]+$}] } - -# :forward do ::interp %1 {% set :interp} :public method do {script} { ::interp eval ${:interp} $script } :public method destroy {} { - # - # TODO: Why am I called twice in doc.test? Because of the test - # enviroment (the auto-cleanup feature?) - # - # puts stderr "SELF [current object] interp ${:interp}" - # ::nsf::__db_show_stack if {${:interp} ne ""} { if {[interp exists ${:interp}]} { interp delete ${:interp} } } else { + # + # TODO: complete the coverage of the cleanup ... + # :do { if {[info commands ::nsf::configure] ne ""} { ::nsf::configure keepinitcmd false; @@ -2777,362 +2627,82 @@ } -namespace eval ::nx::doc::xodoc { - namespace import -force ::nx::* - namespace import -force ::nx::doc::* - # xodoc -> nxdoc - # - - - - - - - - - - - - - - - - - # MetadataToken Entity - # FileToken @package - # PackageToken @package - # ConstraintToken n/a - # MethodToken n/a - # ProcToken @method (scope = object) - # InstprocToken @method (scope = class) - # ObjToken @object - # ClassToken @class - # MetaClassToken n/a - - Class create MetadataToken { - :class property analyzer - :public forward analyzer [current] %method - :method as {partof:object,type=::nx::doc::StructuredEntity} \ - -returns object,type=::nx::doc::Entity { - error "Subclass responsibility" - } - :public method emit {partof:object,type=::nx::doc::StructuredEntity} \ - -returns object,type=::nx::doc::Entity { - set entity [:as $partof] - set props [:get_properties] - if {[dict exists $props description]} { - $entity @doc [dict get $props description] - } - return $entity - } - :method get_properties {} { - if {[info exists :properties]} { - set props [dict create] - foreach p ${:properties} { - if {[info exists :$p]} { - dict set props [string tolower $p] \ - [:format [set :$p]] - } - } - return $props - } - } - :method format {value} { - # - # 1. replace @-prefixed tags etc. - # - set value [[:analyzer] replaceFormatTags $value] - - # - # 2. escape Tcl evaluation chars in code listings - # - set value [string map { - "\\" "\\\\" - "{" "\\{" - "}" "\\}" - "\"" "\\\"" - "[" "\\[" - "]" "\\]" - "$" "\\$" - } $value] - - # - # 3. box the prop value in a list (this avoids unwanted - # interactions with the line-by-line as_text post-processor) - # - return [list $value] - } - } +# +# Validator +# +namespace eval ::nx::doc { - Class create PackageToken -superclass MetadataToken - Class create FileToken -superclass MetadataToken { - :method as {partof:object,type=::nx::doc::StructuredEntity} \ - -returns object,type=::nx::doc::Entity { - # - # TODO: Where to retrieve the package name from? - # - return [@package new -name XOTcl] - } - :public method emit {partof:object,type=::nx::doc::StructuredEntity} \ - -returns object,type=::nx::doc::Entity { - set entity [next] - set props [dict remove [:get_properties] description] - dict for {prop value} $props { - $entity @doc add "