added audit trail to factttable-for-report (fix #126)

import module namespace companies = "http://xbrl.io/modules/bizql/profiles/sec/companies";
import module namespace sec-fiscal = "http://xbrl.io/modules/bizql/profiles/sec/fiscal/core";
import module namespace core = "http://xbrl.io/modules/bizql/profiles/sec/core";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace session = "http://apps.28.io/session";
import module namespace archives = "http://xbrl.io/modules/bizql/archives";
import module namespace entities = "http://xbrl.io/modules/bizql/entities";

import module namespace csv = "http://zorba.io/modules/json-csv";

declare function local:to-csv($o as object*) as string?
{
    if (exists($o)) (: bug in csv:serialize :)
    then
    string-join(
        csv:serialize(
            for $o in $o
            let $a := $o.Aspects
            return {|
                for $k in keys($a)
                return { $k : $a.$k },
                { "Value" : $o.Value },
                if (exists($o."Unit"))
                then { "Unit" :  $o."Unit" }
                else (),
                if (exists($o.Decimals))
                then { "Decimals" : $o.Decimals }
                else(),
                { "EntityRegistrantName" : $o.EntityRegistrantName },
                if (exists($o.ReportedConcept))
                then { "ReportedConcept" : $o.ReportedConcept }
                else ()
            |},
            { serialize-null-as : "" }
        )
    )
    else ()
};

declare function local:audittrail-to-xml($audit as item) as element()
{
    <AuditTrails>{
        for $a in if ($audit instance of array) then $audit[] else $audit
        return (
            <Type>{$a.Type}</Type>,
            <Label>{$a.Label}</Label>,
            <Message>{$a.Label}</Message>,
            <Data>{
                if (exists($a.Data.OriginalConcept))
                then <OriginalConcept>{$a.Data.OriginalConcept}</OriginalConcept>
                else (),
                if (exists($a.Data.OutputConcept))
                then <OutputConcept>{$a.Data.OutputConcept}</OutputConcept>
                else (),
                if (exists($a.Data.AuditTrails))
                then local:audittrail-to-xml($a.Data.AuditTrails)
                else ()
            }</Data>
        )
    }</AuditTrails>
};

declare function local:to-xml($o as object*) as node()*
{
    (session:comment("xml", {
                            NumFacts : count($o),
                            TotalNumFacts: session:num-facts(),
                            TotalNumArchives: session:num-archives(),
                            TotalNumEntities: session:num-entities()
                        }),
    <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
            TableName="xbrl:FactTableForReport">{
        for $o in $o
        let $a := $o.Aspects
        return
            <Fact>{
                <Aspects>{
                    for $k in keys($a)
                    return
                        <Aspect>
                            <Name>{$k}</Name>
                            <Value>{$a.$k}</Value>
                        </Aspect>
                }</Aspects>,
                <Value>{$o.Value}</Value>,
                <Type>{$o.Type}</Type>,
                if (exists($o.Unit))
                then <Unit>{$o.Unit}</Unit>
                else(),
                if (exists($o.Decimals))
                then <Decimals>{$o.Decimals}</Decimals>
                else (),
                <EntityRegistrantName>{$o.EntityRegistrantName}</EntityRegistrantName>,
                if (exists($o.AuditTrails))
                then local:audittrail-to-xml($o.AuditTrails)
                else ()
            }</Fact>
    }</FactTable>)
};

declare function local:filings(
    $ciks as string*,
    $tags as string*,
    $tickers as string*,
    $sics as string*,
    $fp as string*,
    $fy as string*) as object*
{
    let $entities := (
        companies:companies($ciks),
        companies:companies-for-tags($tags),
        companies:companies-for-tickers($tickers),
        companies:companies-for-SIC($sics)
    )
    for $entity in $entities
    for $fy in distinct-values(
                for $fy in $fy
                return
                    switch ($fy)
                    case "LATEST" return
                        for $p in $fp
                        return
                            if ($p eq "FY")
                            then sec-fiscal:latest-reported-fiscal-period($entity, "10-K").year
                            else sec-fiscal:latest-reported-fiscal-period($entity, "10-Q").year
                    case "ALL" return  $sec-fiscal:ALL_FISCAL_YEARS
                    default return $fy
                )
    for $fp in $fp
    return sec-fiscal:filings-for-entities-and-fiscal-periods-and-years($entity, $fp, $fy ! ($$ cast as integer))
};

let $format      := lower-case((request:param-values("format"), substring-after(request:path(), ".jq."))[1])
let $ciks        := distinct-values(companies:eid(request:param-values("cik")))
let $tags        := distinct-values(request:param-values("tag") ! upper-case($$))
let $tickers     := distinct-values(request:param-values("ticker"))
let $sics        := distinct-values(request:param-values("sic"))
let $fiscalYears := distinct-values(
                        for $y in request:param-values("fiscalYear", "LATEST")
                        return
                            if ($y eq "LATEST" or $y eq "ALL")
                            then $y
                            else if ($y castable as integer)
                            then $y
                            else ()
                    )
let $fiscalPeriods := distinct-values(let $fp := request:param-values("fiscalPeriod", "FY")
                      return
                        if (($fp ! lower-case($$)) = "all")
                        then $sec-fiscal:ALL_FISCAL_PERIODS
                        else $fp)
let $aids        := archives:aid(request:param-values("aid"))
let $archives    := (
                        local:filings($ciks, $tags, $tickers, $sics, $fiscalPeriods, $fiscalYears),
                        archives:archives($aids)
                    )
let $entities    := entities:entities($archives.Entity)
let $report := request:param-values("report")
let $facts :=
    for $fact in core:facts-for-schema($report, $archives)
    group by $archive := $fact.Archive
    let $archive := archives:archives($archive)
    let $entity    := entities:entities($archive.Entity)
    let $fp := $archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
    let $fy := $archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus
    for $fact in $fact
    return
    {|
        { Aspects : {|
            for $a in keys($fact.Aspects)
            return { $a : $fact.Aspects.$a },
            { "secxbrl:FiscalPeriod" : $fp },
            { "secxbrl:FiscalYear" : $fy }
        |} },
        { Type: $fact.Type },
        if (exists($fact.Aspects."xbrl:Unit"))
        then { Unit: $fact.Aspects."xbrl:Unit" }
        else  (),
        if (exists($fact.Decimals))
        then { Decimals: $fact.Decimals }
        else (),
        { Value: $fact.Value },
        { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
        { "AuditTrails" : $fact.AuditTrails[] }

    |}
return
    if (session:only-dow30($entities) or session:valid())
        then {
            switch ($format)
            case "xml" return {
                response:serialization-parameters({"omit-xml-declaration" : false, indent : true });
                local:to-xml($facts)
            }
            case "text" case "csv" return {
                response:content-type("text/csv");
                response:header("Content-Disposition", "attachment; filename=facts.csv");
                local:to-csv($facts)
            }
            case "excel" return {
                response:content-type("application/vnd.ms-excel");
                response:header("Content-Disposition", "attachment; filename=fact.csv");
                local:to-csv($facts)
            }
            default return {
                response:content-type("application/json");
                response:serialization-parameters({"indent" : true});
                {|
                    { NetworkIdentifier : "http://secxbrl.info/facts" },
                    { TableName : "xbrl:Facts" },
                    { FactTable : [ $facts ] },
                    session:comment("json", {
                            NumFacts : count($facts),
                            TotalNumFacts: session:num-facts(),
                            TotalNumArchives: session:num-archives(),
                            TotalNumEntities: session:num-entities()
                        })
                |}
            }
        }
        else {
            response:status-code(401);
            session:error("accessing filings of an entity that is not in the DOW30", $format)
        }

import module namespace companies = "http://xbrl.io/modules/bizql/profiles/sec/companies";
import module namespace sec-fiscal = "http://xbrl.io/modules/bizql/profiles/sec/fiscal/core";
import module namespace core = "http://xbrl.io/modules/bizql/profiles/sec/core";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace session = "http://apps.28.io/session";
import module namespace archives = "http://xbrl.io/modules/bizql/archives";
import module namespace entities = "http://xbrl.io/modules/bizql/entities";

import module namespace csv = "http://zorba.io/modules/json-csv";

declare function local:to-csv($o as object*) as string?
{
    if (exists($o)) (: bug in csv:serialize :)
    then
    string-join(
        csv:serialize(
            for $o in $o
            let $a := $o.Aspects
            return {|
                for $k in keys($a)
                return { $k : $a.$k },
                { "Value" : $o.Value },
                if (exists($o."Unit"))
                then { "Unit" :  $o."Unit" }
                else (),
                if (exists($o.Decimals))
                then { "Decimals" : $o.Decimals }
                else(),
                { "EntityRegistrantName" : $o.EntityRegistrantName },
                if (exists($o.ReportedConcept))
                then { "ReportedConcept" : $o.ReportedConcept }
                else ()
            |},
            { serialize-null-as : "" }
        )
    )
    else ()
};

declare function local:to-xml($o as object*) as node()*
{
    (session:comment("xml", {
                            NumFacts : count($o),
                            TotalNumFacts: session:num-facts(),
                            TotalNumArchives: session:num-archives(),
                            TotalNumEntities: session:num-entities()
                        }),
    <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
            TableName="xbrl:FactTableForReport">{
        for $o in $o
        let $a := $o.Aspects
        return
            <Fact>{
                <Aspects>{
                    for $k in keys($a)
                    return
                        <Aspect>
                            <Name>{$k}</Name>
                            <Value>{$a.$k}</Value>
                        </Aspect>
                }</Aspects>,
                <Value>{$o.Value}</Value>,
                <Type>{$o.Type}</Type>,
                if (exists($o.Unit))
                then <Unit>{$o.Unit}</Unit>
                else(),
                if (exists($o.Decimals))
                then <Decimals>{$o.Decimals}</Decimals>
                else (),
                <EntityRegistrantName>{$o.EntityRegistrantName}</EntityRegistrantName>,
                if (exists($o.ReportedConcept))
                then <ReportedConcept>{$o."ReportedConcept"}</ReportedConcept>
                else ()
            }</Fact>
    }</FactTable>)
};

declare function local:filings(
    $ciks as string*,
    $tags as string*,
    $tickers as string*,
    $sics as string*,
    $fp as string*,
    $fy as string*) as object*
{
    let $entities := (
        companies:companies($ciks),
        companies:companies-for-tags($tags),
        companies:companies-for-tickers($tickers),
        companies:companies-for-SIC($sics)
    )
    for $entity in $entities
    for $fy in distinct-values(
                for $fy in $fy
                return
                    switch ($fy)
                    case "LATEST" return
                        for $p in $fp
                        return
                            if ($p eq "FY")
                            then sec-fiscal:latest-reported-fiscal-period($entity, "10-K").year
                            else sec-fiscal:latest-reported-fiscal-period($entity, "10-Q").year
                    case "ALL" return  $sec-fiscal:ALL_FISCAL_YEARS
                    default return $fy
                )
    for $fp in $fp
    return sec-fiscal:filings-for-entities-and-fiscal-periods-and-years($entity, $fp, $fy ! ($$ cast as integer))
};

let $format      := lower-case((request:param-values("format"), substring-after(request:path(), ".jq."))[1])
let $ciks        := distinct-values(companies:eid(request:param-values("cik")))
let $tags        := distinct-values(request:param-values("tag") ! upper-case($$))
let $tickers     := distinct-values(request:param-values("ticker"))
let $sics        := distinct-values(request:param-values("sic"))
let $fiscalYears := distinct-values(
                        for $y in request:param-values("fiscalYear", "LATEST")
                        return
                            if ($y eq "LATEST" or $y eq "ALL")
                            then $y
                            else if ($y castable as integer)
                            then $y
                            else ()
                    )
let $fiscalPeriods := distinct-values(let $fp := request:param-values("fiscalPeriod", "FY")
                      return
                        if (($fp ! lower-case($$)) = "all")
                        then $sec-fiscal:ALL_FISCAL_PERIODS
                        else $fp)
let $aids        := archives:aid(request:param-values("aid"))
let $archives    := (
                        local:filings($ciks, $tags, $tickers, $sics, $fiscalPeriods, $fiscalYears),
                        archives:archives($aids)
                    )
let $entities    := entities:entities($archives.Entity)
let $report := request:param-values("report")
let $facts :=
    for $fact in core:facts-for-schema($report, $archives)
    group by $archive := $fact.Archive
    let $archive := archives:archives($archive)
    let $entity    := entities:entities($archive.Entity)
    let $fp := $archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
    let $fy := $archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus
    for $fact in $fact
    return
    {|
        { Aspects : {|
            for $a in keys($fact.Aspects)
            return { $a : $fact.Aspects.$a },
            { "secxbrl:FiscalPeriod" : $fp },
            { "secxbrl:FiscalYear" : $fy }
        |} },
        { Type: $fact.Type },
        if (exists($fact.Aspects."xbrl:Unit"))
        then { Unit: $fact.Aspects."xbrl:Unit" }
        else  (),
        if (exists($fact.Decimals))
        then { Decimals: $fact.Decimals }
        else (),
        { Value: $fact.Value },
        { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
        { "ReportedConcept" : $fact.AuditTrails[].Data.OriginalConcept[1] }

    |}
return
    if (session:only-dow30($entities) or session:valid())
        then {
            switch ($format)
            case "xml" return {
                response:serialization-parameters({"omit-xml-declaration" : false, indent : true });
                local:to-xml($facts)
            }
            case "text" case "csv" return {
                response:content-type("text/csv");
                response:header("Content-Disposition", "attachment; filename=facts.csv");
                local:to-csv($facts)
            }
            case "excel" return {
                response:content-type("application/vnd.ms-excel");
                response:header("Content-Disposition", "attachment; filename=fact.csv");
                local:to-csv($facts)
            }
            default return {
                response:content-type("application/json");
                response:serialization-parameters({"indent" : true});
                {|
                    { NetworkIdentifier : "http://secxbrl.info/facts" },
                    { TableName : "xbrl:Facts" },
                    { FactTable : [ $facts ] },
                    session:comment("json", {
                            NumFacts : count($facts),
                            TotalNumFacts: session:num-facts(),
                            TotalNumArchives: session:num-archives(),
                            TotalNumEntities: session:num-entities()
                        })
                |}
            }
        }
        else {
            response:status-code(401);
            session:error("accessing filings of an entity that is not in the DOW30", $format)
        }


Removed list of facts in debug part to have smaller results.

jsoniq version "1.0";

module namespace layout = "http://28.io/modules/xbrl/layout";

import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

(:
    TODO: roll-up layout nodes for open structural nodes if desired
:)

(:
    Converts a structural breakdown to a layout header group.
:)
declare %private function layout:breakdown-to-group(
    $breakdown as object,
    $actual-aspect-space as object) as object
{
    let $labels := $breakdown.BreakdownLabels
    let $trees := $breakdown.BreakdownTrees[]
    let $header-rows-for-each-tree :=
        $trees ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
    let $concatenated-header-rows :=
        layout:concatenate-header-rows($header-rows-for-each-tree)
    return {
        GroupLabels: $labels,
        GroupCells: $concatenated-header-rows
    }
};

(:
    Converts a structural node to a header row, and recursively
    converts children as well to bottom header rows.
:)
declare %private function layout:structural-node-to-header-rows(
    $structural-node as object,
    $actual-aspect-space as object) as array
{
    let $bottom-rows :=
        if (exists($structural-node.Children))
        then layout:concatenate-header-rows(
            $structural-node.Children[] ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
        )
        else ()
    let $span as integer :=
        if(exists($bottom-rows))
        then sum(($bottom-rows[[1]])[].CellSpan)
        else 1
    let $open-header-rows :=
        layout:concatenate-header-rows(
            let $constrained-aspect as string := $structural-node.Aspect
            return switch($constrained-aspect)
            case "xbrl:Period" return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                let $tokens := tokenize($actual-value, "/")
                order by $actual-value descending
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value},
                                "table.periodStart" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then string(date($tokens[1]) - dayTimeDuration("P1D"))
                                        else ""
                                },
                                "table.periodEnd" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then $tokens[2]
                                        else ""
                                }
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]
            default return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                order by $actual-value
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value}
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]

        )
    let $closed-header-rows := [
        [ {|
            {
                CellLabels: [ $structural-node.Labels[], values($structural-node.ConstraintSets."") ],
                CellConstraints: ($structural-node.ConstraintSets, {"" : {}})[1],
                TagSelectors: [ $structural-node.TagSelectors[] ],
                CellSpan: $span,
                RollUp: boolean($structural-node.RollUp),
                IsRollUp: ($structural-node.IsTotal, false)[1],
                IsNegated: ($structural-node.IsNegated, false)[1]
            },
           { Id: $structural-node.Id }[exists($structural-node.Id)]
        |} ],
        $bottom-rows[]
    ]
    return
        if($structural-node.Open)
        then $open-header-rows
        else $closed-header-rows
};

(:
    Concatenates groups of header rows to a single group of header rows.
:)
declare function layout:concatenate-header-rows($header-rows-groups as array*) as array
{
    [
        for $row in 1 to size($header-rows-groups[1])
        return [
            for $row-group in $header-rows-groups ! ($$[[$row]])
            return $row-group[]
        ]
    ]
};

(:
    Builds the hypercube to query the facts for layouting.
    It's based on the supplied hypercube if present (intersecting its domains
    with the domains of the table to reduce the result size).
    If no hypercube is given, it will take the table domain.
:)
declare function layout:build-hypercube(
    $structural-model as object,
    $hypercube as object?) as object
{
    let $slice-constraints := accumulate(values(descendant-objects($structural-model).ConstraintSets))
    let $global-constraints := $structural-model.GlobalConstraintSet
    let $open-dimensions := descendant-objects($structural-model).Aspect
    let $table-domain := {|
        for $aspect in distinct-values(keys(($slice-constraints, $global-constraints)))
        return {
            $aspect: [ flatten($slice-constraints.$aspect), flatten($global-constraints.$aspect) ]
        }
    |}
    return hypercubes:user-defined-hypercube(
            {|
                for $dimension in distinct-values((keys(($hypercube.Aspects, $table-domain)), $open-dimensions))
                let $dimension-type as string? := $dimension.Type
                let $hypercube-members as atomic* := (
                    descendant-objects($hypercube.Aspects.$dimension.Domains).Name,
                    $hypercube.Aspects.$dimension.DomainRestriction.Enumeration[])
                let $hypercube-default := ($hypercube.Aspects.$dimension.Default,
                                           $structural-model.DimensionDefaults.$dimension)[1]

                let $table-domain-members := $table-domain.$dimension[]

                let $actual-members :=
                    switch(true)
                    case exists($hypercube-members) and exists($table-domain-members)
                        return $hypercube-members[$$ = $table-domain-members]
                    case exists($hypercube-members)
                        return $hypercube-members
                    case exists($table-domain-members)
                        return $table-domain-members
                    default return ()

                let $inferred-type as string? :=
                    typeswitch($actual-members[not ($$ instance of null)])
                    case string* return
                            if(contains(($actual-members)[not ($$ instance of null)][1], ":"))
                            then ()
                            else "string"
                    case int* return "int"
                    case integer* return "integer"
                    case boolean* return "boolean"
                    default return "atomic"
                let $actual-type as string? := ($dimension-type, $inferred-type)[1]

                return {
                    $dimension : {|
                        { Type : $actual-type }[exists($actual-type) and exists($actual-members)],

                        if(exists($actual-members))
                        then {
                            Domain: [ distinct-values(($actual-members, $hypercube-default)) ]
                        }
                        else (),

                        if(exists($hypercube-default))
                        then {
                            Default: $hypercube-default
                        }
                        else ()
                    |}
                }
            |}
        )
};

(:
    Filters the facts that match the supplied aspect
    constraints. Filters for default if a participating
    aspect is not present in the aspect constraints.
:)
declare function layout:filter-aspects(
    $facts as object*,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object) as object*
{
    for $fact in $facts
    where layout:matches-aspects($fact, $aspect-constraints, $participating-aspects, $defaults)
    return $fact
};

(:
    Checks whether a fact matches the aspect constraints.
:)
declare function layout:matches-aspects(
    $fact as object,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object?) as boolean
{
    (
        every $aspect in $participating-aspects
        satisfies
            let $default-value := $defaults.$aspect
            let $allowed-value := $aspect-constraints.$aspect
            return (
                exists($allowed-value)
                or
                $fact.Aspects.$aspect eq $default-value
            )
    )
    and
    (
        every $aspect in keys($aspect-constraints)
        satisfies
            let $allowed-value := $aspect-constraints.$aspect
            return $fact.Aspects.$aspect eq $allowed-value
    )
};

(:
    Returns the slices implied by the headers with the given tag selectors.
:)
declare function layout:slices($table-headers as array, $tag-selectors as string*) as object*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-slices :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        let $tag-matches := $tag-selectors[$$ = keys($first-row-cell.CellConstraints)]
        return
            switch(count($tag-matches))
            case 0 return (1 to $span) ! ($first-row-cell.CellConstraints."", {})[1]
            case 1 return (1 to $span) ! ($first-row-cell.CellConstraints.$tag-matches, {})[1]
            default return error(
                QName("layout:MORE-THAN-ONE-MATCHING-TAG"),
                "Several tag selectors apply to a cell: " || string-join($tag-matches, " ")
            )
    return
    if(size($table-headers) le 1)
    then $first-slices
    else
        let $bottom-slices := layout:slices($bottom-rows-of-headers, $tag-selectors)
        for $i in 1 to count($first-slices)
        return
          facts:merge-objects($bottom-slices[$i], $first-slices[$i], true)
};

(:
    Returns the sequence of tag selector arrays implied by the headers.
:)
declare function layout:tag-selectors($table-headers as array) as array*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-tags :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        return (1 to $span) ! [ $first-row-cell.TagSelectors[] ]
    return
    if(size($table-headers) le 1)
    then $first-tags
    else
        let $bottom-tags := layout:tag-selectors($bottom-rows-of-headers)
        for $i in 1 to count($first-tags)
        return [($first-tags[$i])[], ($bottom-tags[$i])[]]
};

(:
    Merges the supplied header row groups by projecting them right-to-left.
:)
declare function layout:project($header-rows-groups as object*) as object*
{
    switch(count($header-rows-groups))
    case 0 return {
        GroupLabels: [],
        GroupCells: [
            [
                {
                    CellLabels: [],
                    CellConstraints: { "": {} },
                    CellSpan: 1
                }
                ]
        ]
    }
    case 1 return $header-rows-groups
    default return
        let $first := head($header-rows-groups)
        let $first-size := layout:header-rows-width($first)
        let $bottom := layout:project(tail($header-rows-groups))
        let $bottom-size as integer := layout:header-rows-width(head($bottom))
        return (
            layout:stretch-header-rows($first, $bottom-size),
            $bottom ! layout:duplicate-header-rows($$, $first-size)
        )
};

(:
    Returns the width of a header row group.
:)
declare function layout:header-rows-width($header-rows-group as object) as integer
{
    sum(($header-rows-group.GroupCells[[1]])[].CellSpan)
};

(:
    Stretches a header row group by the desired factor.
:)
declare function layout:stretch-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            for $cell in $row[]
            return copy $c := $cell
                   modify replace value of json $c.CellSpan with $c.CellSpan * $factor
                   return $c
        ]
    ]
    return $g
};

(:
    Duplicate the supplied rows the number of times specified in factor.
:)
declare function layout:duplicate-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            (1 to $factor) ! $row[]
        ]
    ]
    return $g
};

(:
   Returns the numbers of empty rows and columns
:)
declare function layout:empty-rows-and-columns($cells as array) as object
{
    {
        Rows: [
            for $i in 1 to size($cells)
            let $row := ($cells[[$i]])[]
            where every $cell in $row satisfies empty($cell.Value)
            return $i
        ],
        Columns: [
            for $j in 1 to size($cells[[1]])
            let $column := $cells[][[$j]]
            where every $cell in $column satisfies empty($cell.Value)
            return $j
        ]
    }
};

(:
    Eliminates empty rows/columns in the table headers.
:)
declare function layout:eliminate-table-headers($table-headers as object, $empty-rows-and-columns as object) as object
{
    copy $th := $table-headers
    modify let $x-headers as array* := $th.x[].GroupCells
           let $x-eliminated-headers as array* := layout:eliminate($x-headers, $empty-rows-and-columns.Columns[])
           let $y-headers as array* := $th.y[].GroupCells
           let $y-eliminated-headers as array* := layout:eliminate($y-headers, $empty-rows-and-columns.Rows[])
           return (
               for $i in 1 to size($th.x)
               return replace value of json $th.x[[$i]].GroupCells with $x-eliminated-headers[$i],
               for $j in 1 to size($th.y)
               return replace value of json $th.y[[$j]].GroupCells with $y-eliminated-headers[$j]
           )
    return $th
};

(:
    Eliminates empty rows/columns in the cells.
:)
declare function layout:eliminate-cells($cells as array, $empty-rows-and-columns as object) as array
{
    [
        for $row at $i in $cells[]
        where not $i = $empty-rows-and-columns.Rows[]
        return [
            for $cell as item at $j in $row[]
            where not $j = $empty-rows-and-columns.Columns[]
            return $cell
        ]
    ]
};

(:
    Eliminates empty rows/columns in the table header's group cells.
:)
declare function layout:eliminate($header-groups as array*, $empty-columns as integer*) as array*
{
    let $first-group as array? := head($header-groups)
    let $other-groups as array* := tail($header-groups)
    return if(empty($first-group))
           then ()
           else
               let $new-first-group as array := [
                  for $row as array in $first-group[]
                  return [
                       for $i in 1 to size($row)
                       let $lower-bound as integer :=  sum((0, $row[][position() lt $i].CellSpan))
                       let $upper-bound as integer :=  sum($row[][position() le $i].CellSpan)
                       let $cell as object := $row[[$i]]
                       let $matching-empty-columns := $empty-columns[$lower-bound lt $$ and $$ le $upper-bound]
                       let $number := count($matching-empty-columns)
                       return switch(true)
                              case $cell.CellSpan eq $number
                              return ()
                              case $cell.CellSpan gt $number
                              return copy $c := $cell modify replace value of json $c.CellSpan with $c.CellSpan - $number return $c
                              default return error(QName("layout:ELIMINATION-ERROR"), ($number || " > " || $cell.CellSpan))
                    ]
               ]
               return (
                   $new-first-group,
                   layout:eliminate($other-groups, $empty-columns)
               )
};

(:~
 : Converts a structural model to a layout model.
 :
 : @param $structural-model a structural model.
 : @options - hypercube: a hypercube (null for using table domain, default: dimensionless hypercube)
 :          - filter
 :
 : @error layout:MORE-THAN-ONE-MATCHING-TAG in case of a tag selector conflict.
 : @error layout:ELIMINATION_ERROR an error occurred during elimination of empty rows/columns.
 :
 : @return a layout model.
 :)
declare function layout:layout(
    $structural-model as object,
    $options as object?
) as object
{
    let $elimination as boolean := ($options.Eliminate, false)[1]
    let $original-hypercube :=
        if($options.Hypercube instance of null)
        then ()
        else ($options.Hypercube, hypercubes:dimensionless-hypercube())[1]
    let $hypercube := layout:build-hypercube(
        $structural-model,
        $original-hypercube
    )
    let $facts := facts:facts-for({|
        {Hypercube: $hypercube},
        trim($options, "Hypercube")
    |})
    let $actual-aspect-space :=
    {|
        for $aspect in distinct-values(keys($facts.Aspects))
        return { $aspect : [ distinct-values($facts.Aspects.$aspect) ] }
    |}
    let $defaults :=
    {|
        for $aspect in keys($hypercube.Aspects)
        let $default := $hypercube.Aspects.$aspect.Default
        where exists($default)
        return { $aspect : $default }
    |}
    return {
        ModelKind: "LayoutModel",
        TableSetLabels: $structural-model.TableSetLabels,
        TableSet: [
            for $table in $structural-model.TableSet[]
            let $table-headers as object := {|
                for $axis in keys($table.Breakdowns)
                return {
                    $axis: [
                        layout:project(
                            for $breakdown in $table.Breakdowns.$axis[]
                            return layout:breakdown-to-group($breakdown, $actual-aspect-space)
                        )
                    ]
                }
            |}
            let $x-tag-selectors := layout:tag-selectors([$table-headers.x[].GroupCells[]])
            let $y-tag-selectors := layout:tag-selectors([$table-headers.y[].GroupCells[]])
            let $default-x-slices := layout:slices([$table-headers.x[].GroupCells[]], ())
            let $default-y-slices := layout:slices([$table-headers.y[].GroupCells[]], ())
            let $period-start-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodStart")
            let $period-start-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodStart")
            let $period-end-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodEnd")
            let $period-end-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodEnd")
            let $cells := [
                for $y-tag-selector at $i in $y-tag-selectors
                return [
                    for $x-tag-selector at $j in $x-tag-selectors
                    let $selectors as string* := ($x-tag-selector[], $y-tag-selector[])
                    let $x-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-x-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-x-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-x-slices
                        default return layout:slices([$table-headers.x[].GroupCells[]], $selectors)
                    let $participating-x := distinct-values(keys($x-slices))
                    let $y-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-y-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-y-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-y-slices
                        default return layout:slices([$table-headers.y[].GroupCells[]], $selectors)
                    let $participating-y := distinct-values(keys($y-slices))
                    let $row-facts := layout:filter-aspects(
                        $facts,
                        $y-slices[$i],
                        $participating-y,
                        $defaults)
                    let $cell-facts := layout:filter-aspects(
                        $row-facts,
                        $x-slices[$j],
                        $participating-x,
                        $defaults)
                    return typeswitch($cell-facts)
                           case object return $cell-facts
                           case () return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                           case object+ return [ $cell-facts ]
                           default return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                ]
            ]
            let $empty-rows-and-columns := layout:empty-rows-and-columns($cells)
            let $table-headers := if($elimination)
                                  then layout:eliminate-table-headers($table-headers, $empty-rows-and-columns)
                                  else $table-headers
            let $cells := if($elimination)
                          then layout:eliminate-cells($cells, $empty-rows-and-columns)
                          else $cells
            return
            {
                TableHeaders: $table-headers,
                TableCells: {
                    AxisOrder: [ "y", "x" ],
                    Facts: $cells
                }
            }
        ],
        GlobalConstraints: $structural-model.GlobalConstraintSet,
        GlobalConstraintLabels: $structural-model.GlobalConstraintLabels,
        DebugInfo: {
            Hypercube: $hypercube,
            OriginalHypercube: $original-hypercube,
        }
    }
};

jsoniq version "1.0";

module namespace layout = "http://28.io/modules/xbrl/layout";

import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

(:
    TODO: roll-up layout nodes for open structural nodes if desired
:)

(:
    Converts a structural breakdown to a layout header group.
:)
declare %private function layout:breakdown-to-group(
    $breakdown as object,
    $actual-aspect-space as object) as object
{
    let $labels := $breakdown.BreakdownLabels
    let $trees := $breakdown.BreakdownTrees[]
    let $header-rows-for-each-tree :=
        $trees ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
    let $concatenated-header-rows :=
        layout:concatenate-header-rows($header-rows-for-each-tree)
    return {
        GroupLabels: $labels,
        GroupCells: $concatenated-header-rows
    }
};

(:
    Converts a structural node to a header row, and recursively
    converts children as well to bottom header rows.
:)
declare %private function layout:structural-node-to-header-rows(
    $structural-node as object,
    $actual-aspect-space as object) as array
{
    let $bottom-rows :=
        if (exists($structural-node.Children))
        then layout:concatenate-header-rows(
            $structural-node.Children[] ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
        )
        else ()
    let $span as integer :=
        if(exists($bottom-rows))
        then sum(($bottom-rows[[1]])[].CellSpan)
        else 1
    let $open-header-rows :=
        layout:concatenate-header-rows(
            let $constrained-aspect as string := $structural-node.Aspect
            return switch($constrained-aspect)
            case "xbrl:Period" return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                let $tokens := tokenize($actual-value, "/")
                order by $actual-value descending
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value},
                                "table.periodStart" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then string(date($tokens[1]) - dayTimeDuration("P1D"))
                                        else ""
                                },
                                "table.periodEnd" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then $tokens[2]
                                        else ""
                                }
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]
            default return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                order by $actual-value
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value}
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]

        )
    let $closed-header-rows := [
        [ {|
            {
                CellLabels: [ $structural-node.Labels[], values($structural-node.ConstraintSets."") ],
                CellConstraints: ($structural-node.ConstraintSets, {"" : {}})[1],
                TagSelectors: [ $structural-node.TagSelectors[] ],
                CellSpan: $span,
                RollUp: boolean($structural-node.RollUp),
                IsRollUp: ($structural-node.IsTotal, false)[1],
                IsNegated: ($structural-node.IsNegated, false)[1]
            },
           { Id: $structural-node.Id }[exists($structural-node.Id)]
        |} ],
        $bottom-rows[]
    ]
    return
        if($structural-node.Open)
        then $open-header-rows
        else $closed-header-rows
};

(:
    Concatenates groups of header rows to a single group of header rows.
:)
declare function layout:concatenate-header-rows($header-rows-groups as array*) as array
{
    [
        for $row in 1 to size($header-rows-groups[1])
        return [
            for $row-group in $header-rows-groups ! ($$[[$row]])
            return $row-group[]
        ]
    ]
};

(:
    Builds the hypercube to query the facts for layouting.
    It's based on the supplied hypercube if present (intersecting its domains
    with the domains of the table to reduce the result size).
    If no hypercube is given, it will take the table domain.
:)
declare function layout:build-hypercube(
    $structural-model as object,
    $hypercube as object?) as object
{
    let $slice-constraints := accumulate(values(descendant-objects($structural-model).ConstraintSets))
    let $global-constraints := $structural-model.GlobalConstraintSet
    let $open-dimensions := descendant-objects($structural-model).Aspect
    let $table-domain := {|
        for $aspect in distinct-values(keys(($slice-constraints, $global-constraints)))
        return {
            $aspect: [ flatten($slice-constraints.$aspect), flatten($global-constraints.$aspect) ]
        }
    |}
    return hypercubes:user-defined-hypercube(
            {|
                for $dimension in distinct-values((keys(($hypercube.Aspects, $table-domain)), $open-dimensions))
                let $dimension-type as string? := $dimension.Type
                let $hypercube-members as atomic* := (
                    descendant-objects($hypercube.Aspects.$dimension.Domains).Name,
                    $hypercube.Aspects.$dimension.DomainRestriction.Enumeration[])
                let $hypercube-default := ($hypercube.Aspects.$dimension.Default,
                                           $structural-model.DimensionDefaults.$dimension)[1]

                let $table-domain-members := $table-domain.$dimension[]

                let $actual-members :=
                    switch(true)
                    case exists($hypercube-members) and exists($table-domain-members)
                        return $hypercube-members[$$ = $table-domain-members]
                    case exists($hypercube-members)
                        return $hypercube-members
                    case exists($table-domain-members)
                        return $table-domain-members
                    default return ()

                let $inferred-type as string? :=
                    typeswitch($actual-members[not ($$ instance of null)])
                    case string* return
                            if(contains(($actual-members)[not ($$ instance of null)][1], ":"))
                            then ()
                            else "string"
                    case int* return "int"
                    case integer* return "integer"
                    case boolean* return "boolean"
                    default return "atomic"
                let $actual-type as string? := ($dimension-type, $inferred-type)[1]

                return {
                    $dimension : {|
                        { Type : $actual-type }[exists($actual-type) and exists($actual-members)],

                        if(exists($actual-members))
                        then {
                            Domain: [ distinct-values(($actual-members, $hypercube-default)) ]
                        }
                        else (),

                        if(exists($hypercube-default))
                        then {
                            Default: $hypercube-default
                        }
                        else ()
                    |}
                }
            |}
        )
};

(:
    Filters the facts that match the supplied aspect
    constraints. Filters for default if a participating
    aspect is not present in the aspect constraints.
:)
declare function layout:filter-aspects(
    $facts as object*,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object) as object*
{
    for $fact in $facts
    where layout:matches-aspects($fact, $aspect-constraints, $participating-aspects, $defaults)
    return $fact
};

(:
    Checks whether a fact matches the aspect constraints.
:)
declare function layout:matches-aspects(
    $fact as object,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object?) as boolean
{
    (
        every $aspect in $participating-aspects
        satisfies
            let $default-value := $defaults.$aspect
            let $allowed-value := $aspect-constraints.$aspect
            return (
                exists($allowed-value)
                or
                $fact.Aspects.$aspect eq $default-value
            )
    )
    and
    (
        every $aspect in keys($aspect-constraints)
        satisfies
            let $allowed-value := $aspect-constraints.$aspect
            return $fact.Aspects.$aspect eq $allowed-value
    )
};

(:
    Returns the slices implied by the headers with the given tag selectors.
:)
declare function layout:slices($table-headers as array, $tag-selectors as string*) as object*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-slices :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        let $tag-matches := $tag-selectors[$$ = keys($first-row-cell.CellConstraints)]
        return
            switch(count($tag-matches))
            case 0 return (1 to $span) ! ($first-row-cell.CellConstraints."", {})[1]
            case 1 return (1 to $span) ! ($first-row-cell.CellConstraints.$tag-matches, {})[1]
            default return error(
                QName("layout:MORE-THAN-ONE-MATCHING-TAG"),
                "Several tag selectors apply to a cell: " || string-join($tag-matches, " ")
            )
    return
    if(size($table-headers) le 1)
    then $first-slices
    else
        let $bottom-slices := layout:slices($bottom-rows-of-headers, $tag-selectors)
        for $i in 1 to count($first-slices)
        return
          facts:merge-objects($bottom-slices[$i], $first-slices[$i], true)
};

(:
    Returns the sequence of tag selector arrays implied by the headers.
:)
declare function layout:tag-selectors($table-headers as array) as array*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-tags :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        return (1 to $span) ! [ $first-row-cell.TagSelectors[] ]
    return
    if(size($table-headers) le 1)
    then $first-tags
    else
        let $bottom-tags := layout:tag-selectors($bottom-rows-of-headers)
        for $i in 1 to count($first-tags)
        return [($first-tags[$i])[], ($bottom-tags[$i])[]]
};

(:
    Merges the supplied header row groups by projecting them right-to-left.
:)
declare function layout:project($header-rows-groups as object*) as object*
{
    switch(count($header-rows-groups))
    case 0 return {
        GroupLabels: [],
        GroupCells: [
            [
                {
                    CellLabels: [],
                    CellConstraints: { "": {} },
                    CellSpan: 1
                }
                ]
        ]
    }
    case 1 return $header-rows-groups
    default return
        let $first := head($header-rows-groups)
        let $first-size := layout:header-rows-width($first)
        let $bottom := layout:project(tail($header-rows-groups))
        let $bottom-size as integer := layout:header-rows-width(head($bottom))
        return (
            layout:stretch-header-rows($first, $bottom-size),
            $bottom ! layout:duplicate-header-rows($$, $first-size)
        )
};

(:
    Returns the width of a header row group.
:)
declare function layout:header-rows-width($header-rows-group as object) as integer
{
    sum(($header-rows-group.GroupCells[[1]])[].CellSpan)
};

(:
    Stretches a header row group by the desired factor.
:)
declare function layout:stretch-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            for $cell in $row[]
            return copy $c := $cell
                   modify replace value of json $c.CellSpan with $c.CellSpan * $factor
                   return $c
        ]
    ]
    return $g
};

(:
    Duplicate the supplied rows the number of times specified in factor.
:)
declare function layout:duplicate-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            (1 to $factor) ! $row[]
        ]
    ]
    return $g
};

(:
   Returns the numbers of empty rows and columns
:)
declare function layout:empty-rows-and-columns($cells as array) as object
{
    {
        Rows: [
            for $i in 1 to size($cells)
            let $row := ($cells[[$i]])[]
            where every $cell in $row satisfies empty($cell.Value)
            return $i
        ],
        Columns: [
            for $j in 1 to size($cells[[1]])
            let $column := $cells[][[$j]]
            where every $cell in $column satisfies empty($cell.Value)
            return $j
        ]
    }
};

(:
    Eliminates empty rows/columns in the table headers.
:)
declare function layout:eliminate-table-headers($table-headers as object, $empty-rows-and-columns as object) as object
{
    copy $th := $table-headers
    modify let $x-headers as array* := $th.x[].GroupCells
           let $x-eliminated-headers as array* := layout:eliminate($x-headers, $empty-rows-and-columns.Columns[])
           let $y-headers as array* := $th.y[].GroupCells
           let $y-eliminated-headers as array* := layout:eliminate($y-headers, $empty-rows-and-columns.Rows[])
           return (
               for $i in 1 to size($th.x)
               return replace value of json $th.x[[$i]].GroupCells with $x-eliminated-headers[$i],
               for $j in 1 to size($th.y)
               return replace value of json $th.y[[$j]].GroupCells with $y-eliminated-headers[$j]
           )
    return $th
};

(:
    Eliminates empty rows/columns in the cells.
:)
declare function layout:eliminate-cells($cells as array, $empty-rows-and-columns as object) as array
{
    [
        for $row at $i in $cells[]
        where not $i = $empty-rows-and-columns.Rows[]
        return [
            for $cell as item at $j in $row[]
            where not $j = $empty-rows-and-columns.Columns[]
            return $cell
        ]
    ]
};

(:
    Eliminates empty rows/columns in the table header's group cells.
:)
declare function layout:eliminate($header-groups as array*, $empty-columns as integer*) as array*
{
    let $first-group as array? := head($header-groups)
    let $other-groups as array* := tail($header-groups)
    return if(empty($first-group))
           then ()
           else
               let $new-first-group as array := [
                  for $row as array in $first-group[]
                  return [
                       for $i in 1 to size($row)
                       let $lower-bound as integer :=  sum((0, $row[][position() lt $i].CellSpan))
                       let $upper-bound as integer :=  sum($row[][position() le $i].CellSpan)
                       let $cell as object := $row[[$i]]
                       let $matching-empty-columns := $empty-columns[$lower-bound lt $$ and $$ le $upper-bound]
                       let $number := count($matching-empty-columns)
                       return switch(true)
                              case $cell.CellSpan eq $number
                              return ()
                              case $cell.CellSpan gt $number
                              return copy $c := $cell modify replace value of json $c.CellSpan with $c.CellSpan - $number return $c
                              default return error(QName("layout:ELIMINATION-ERROR"), ($number || " > " || $cell.CellSpan))
                    ]
               ]
               return (
                   $new-first-group,
                   layout:eliminate($other-groups, $empty-columns)
               )
};

(:~
 : Converts a structural model to a layout model.
 :
 : @param $structural-model a structural model.
 : @options - hypercube: a hypercube (null for using table domain, default: dimensionless hypercube)
 :          - filter
 :
 : @error layout:MORE-THAN-ONE-MATCHING-TAG in case of a tag selector conflict.
 : @error layout:ELIMINATION_ERROR an error occurred during elimination of empty rows/columns.
 :
 : @return a layout model.
 :)
declare function layout:layout(
    $structural-model as object,
    $options as object?
) as object
{
    let $elimination as boolean := ($options.Eliminate, false)[1]
    let $original-hypercube :=
        if($options.Hypercube instance of null)
        then ()
        else ($options.Hypercube, hypercubes:dimensionless-hypercube())[1]
    let $hypercube := layout:build-hypercube(
        $structural-model,
        $original-hypercube
    )
    let $facts := facts:facts-for({|
        {Hypercube: $hypercube},
        trim($options, "Hypercube")
    |})
    let $actual-aspect-space :=
    {|
        for $aspect in distinct-values(keys($facts.Aspects))
        return { $aspect : [ distinct-values($facts.Aspects.$aspect) ] }
    |}
    let $defaults :=
    {|
        for $aspect in keys($hypercube.Aspects)
        let $default := $hypercube.Aspects.$aspect.Default
        where exists($default)
        return { $aspect : $default }
    |}
    return {
        ModelKind: "LayoutModel",
        TableSetLabels: $structural-model.TableSetLabels,
        TableSet: [
            for $table in $structural-model.TableSet[]
            let $table-headers as object := {|
                for $axis in keys($table.Breakdowns)
                return {
                    $axis: [
                        layout:project(
                            for $breakdown in $table.Breakdowns.$axis[]
                            return layout:breakdown-to-group($breakdown, $actual-aspect-space)
                        )
                    ]
                }
            |}
            let $x-tag-selectors := layout:tag-selectors([$table-headers.x[].GroupCells[]])
            let $y-tag-selectors := layout:tag-selectors([$table-headers.y[].GroupCells[]])
            let $default-x-slices := layout:slices([$table-headers.x[].GroupCells[]], ())
            let $default-y-slices := layout:slices([$table-headers.y[].GroupCells[]], ())
            let $period-start-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodStart")
            let $period-start-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodStart")
            let $period-end-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodEnd")
            let $period-end-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodEnd")
            let $cells := [
                for $y-tag-selector at $i in $y-tag-selectors
                return [
                    for $x-tag-selector at $j in $x-tag-selectors
                    let $selectors as string* := ($x-tag-selector[], $y-tag-selector[])
                    let $x-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-x-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-x-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-x-slices
                        default return layout:slices([$table-headers.x[].GroupCells[]], $selectors)
                    let $participating-x := distinct-values(keys($x-slices))
                    let $y-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-y-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-y-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-y-slices
                        default return layout:slices([$table-headers.y[].GroupCells[]], $selectors)
                    let $participating-y := distinct-values(keys($y-slices))
                    let $row-facts := layout:filter-aspects(
                        $facts,
                        $y-slices[$i],
                        $participating-y,
                        $defaults)
                    let $cell-facts := layout:filter-aspects(
                        $row-facts,
                        $x-slices[$j],
                        $participating-x,
                        $defaults)
                    return typeswitch($cell-facts)
                           case object return $cell-facts
                           case () return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                           case object+ return [ $cell-facts ]
                           default return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                ]
            ]
            let $empty-rows-and-columns := layout:empty-rows-and-columns($cells)
            let $table-headers := if($elimination)
                                  then layout:eliminate-table-headers($table-headers, $empty-rows-and-columns)
                                  else $table-headers
            let $cells := if($elimination)
                          then layout:eliminate-cells($cells, $empty-rows-and-columns)
                          else $cells
            return
            {
                TableHeaders: $table-headers,
                TableCells: {
                    AxisOrder: [ "y", "x" ],
                    Facts: $cells
                }
            }
        ],
        GlobalConstraints: $structural-model.GlobalConstraintSet,
        GlobalConstraintLabels: $structural-model.GlobalConstraintLabels,
        DebugInfo: {
            Hypercube: $hypercube,
            OriginalHypercube: $original-hypercube,
            Facts: $facts
        }
    }
};


Update facts.jq

Update facts.jq

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace test = "http://apps.28.io/test";

declare %an:nondeterministic function local:test-facttable($expected as integer, $params as object) as item
{
    let $endpoint := "facts"
    let $request := test:invoke($endpoint, $params)
    let $actual as integer := count($request[2].FactTable[])
    let $status as integer := $request[1]
    return test:assert-eq($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-empty($params as object) as item
{
    let $endpoint := "facts"
    let $res as object := test:invoke-raw($endpoint, $params)
    return if($res.status eq 200 and $res.headers."Content-Length" eq "0") then true else {
        url: test:url($endpoint, $params),
        unexpectedResponse: $res
    }
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

declare %an:nondeterministic function local:test-labels() as item
{
    let $endpoint := "facts"
    let $params := {
        concept: [ "fac:Assets", "us-gaap:CashAndCashEquivalentsAtCarryingValue" ],
        report: "FundamentalAccountingConcepts",
        format: "csv",
        ticker: "ko",
        fiscalYear: 2013,
        fiscalPeriod: "Q3",
        labels: true
    }
    let $res as object := test:invoke-raw($endpoint, $params)
    let $actual := $res.body.content
    let $expectedLines := (
        "Accession Number,Concept,Entity,Period,Fiscal Period,Fiscal Year,Accepted,Legal Entity,Unit,Value,Decimals",
        "0000021344-13-000050,\"Cash and Cash Equivalents, at Carrying Value\",COCA COLA CO,2013-09-27,Q3,2013,20131024121047,Default Legal Entity,USD,11118000000,-6",
        "0000021344-13-000050,Assets,COCA COLA CO,2013-09-27,Q3,2013,20131024121047,Default Legal Entity,USD,89432000000,-6"
    )
    return if($res.status eq 200 and (every $line in $expectedLines satisfies contains($actual,$line))) then true else {
        url: test:url($endpoint, $params),
        unexpectedResponse: $res
    }
};

local:check({
    cocacola: local:test-facttable(468, {
        ticker:"ko"
    }),
    cocacolaCSVLabels: local:test-labels(),
    tickerconcept: local:test-facttable(1, {
        ticker:"ko",
        concept:"us-gaap:Assets"
    }),
    tickerfyfprole: local:test-facttable(1, {
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        concept:"us-gaap:Assets"
    }),
    tagconcept: local:test-facttable(30, {
        tag:"DOW30",
        concept:"us-gaap:Assets"
    }),
    tagfyfprole: local:test-facttable(30, {
        tag:"DOW30",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        concept:"us-gaap:Assets"
    }),
    testNoEntity4Sic: local:test-empty({
        concept:"fac:Assets",
        map:"FundamentalAccountingConcepts",
        format:"csv",
        sic:"5052",
        fiscalYear:"2010",
        fiscalPeriod:"Q3"
    }),
    quartersfact: local:test-facttable(0, { (: this fact is not reported in the raw XML filing :)
        aid: "0000051143-13-000007",
        concept: "disc:AssetRetirementObligationsPolicy",
        fiscalPeriod: "Q3",
        fiscalYear: "2013",
        map: "Disclosures"
    }),
    quartersfact2: local:test-facttable(1, {
        aid: "0001193125-12-207154",
        concept: "disc:CashFlowOperatingCapitalTableTextBlock",
        fiscalPeriod: "Q1",
        fiscalYear: "2012",
        map: "Disclosures"
    }),
    generic: local:test-facttable(2, {
        "xbrl:Entity":"http://www.sec.gov/CIK%200000021344",
        "sec:Accepted":"20140227132423",
        concept:"us-gaap:Assets",
        profile-name: "generic"
    })
})

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace test = "http://apps.28.io/test";

declare %an:nondeterministic function local:test-facttable($expected as integer, $params as object) as item
{
    let $endpoint := "facts"
    let $request := test:invoke($endpoint, $params)
    let $actual as integer := count($request[2].FactTable[])
    let $status as integer := $request[1]
    return test:assert-eq($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-empty($params as object) as item
{
    let $endpoint := "facts"
    let $res as object := test:invoke-raw($endpoint, $params)
    return if($res.status eq 200 and $res.headers."Content-Length" eq "0") then true else {
        url: test:url($endpoint, $params),
        unexpectedResponse: $res
    }
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

declare %an:nondeterministic function local:test-labels() as item
{
    let $endpoint := "facts"
    let $params := {
        concept: [ "fac:Assets", "us-gaap:CashAndCashEquivalentsAtCarryingValue" ],
        report: "FundamentalAccountingConcepts",
        format: "csv",
        ticker: "ko",
        fiscalYear: 2013,
        fiscalPeriod: "Q3",
        labels: true
    }
    let $res as object := test:invoke-raw($endpoint, $params)
    let $actual := $res.body.content
    let $expectedLines := (
        "Accession Number,Concept,Entity,Period,Fiscal Period,Fiscal Year,Accepted,Legal Entity,Unit,Value,Decimals",
        "0000021344-13-000050,\"Cash and Cash Equivalents, at Carrying Value\",COCA COLA CO,2013-09-27,Q3,2013,20131024121047,Default Legal Entity,USD,11118000000,-6",
        "0000021344-13-000050,Assets,COCA COLA CO,2013-09-27,Q3,2013,20131024121047,Default Legal Entity,USD,89432000000,-6"
    )
    return if($res.status eq 200 and (every $line in $expectedLines satisfies contains($actual,$line))) then true else {
        url: test:url($endpoint, $params),
        unexpectedResponse: $res
    }
};

local:check({
    cocacola: local:test-facttable(468, {
        ticker:"ko"
    }),
    cocacolaCSVLabels: local:test-labels(),
    tickerconcept: local:test-facttable(1, {
        ticker:"ko",
        concept:"us-gaap:Assets"
    }),
    tickerfyfprole: local:test-facttable(1, {
        ticker:"ko",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        concept:"us-gaap:Assets"
    }),
    tagconcept: local:test-facttable(30, {
        tag:"DOW30",
        concept:"us-gaap:Assets"
    }),
    tagfyfprole: local:test-facttable(30, {
        tag:"DOW30",
        fiscalYear:"2012",
        fiscalPeriod:"Q1",
        concept:"us-gaap:Assets"
    }),
    testNoEntity4Sic: local:test-empty({
        concept:"fac:Assets",
        map:"FundamentalAccountingConcepts",
        format:"csv",
        sic:"5052",
        fiscalYear:"2010",
        fiscalPeriod:"Q3"
    }),
    quartersfact: local:test-facttable(0, { (: this fact is not reported in the raw XML filing :)
        aid: "0000051143-13-000007",
        concept: "disc:AssetRetirementObligationsPolicy",
        fiscalPeriod: "Q3",
        fiscalYear: "2013",
        map: "Disclosures"
    }),
    quartersfact2: local:test-facttable(1, {
        aid: "0001193125-12-207154",
        concept: "disc:CashFlowOperatingCapitalTableTextBlock",
        fiscalPeriod: "Q1",
        fiscalYear: "2012",
        map: "Disclosures"
    }),
    generic: local:test-facttable(2, {
        "xbrl:Entity":"http://www.sec.gov/CIK 0000021344",
        "sec:Accepted":"20140227132423",
        concept:"us-gaap:Assets",
        profile-name: "generic"
    })
})


:lipstick: Refactoring facttable-for-report.jq.

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $validate      as boolean external := false;
declare  %rest:case-insensitive                 variable $labels        as boolean external := false;
declare  %rest:case-insensitive                 variable $report        as string? external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;
declare  %rest:case-insensitive                 variable $language      as string  external := "en-US";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $fiscalPeriodType as string* := api:preprocess-fiscal-period-types($fiscalPeriodType)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic, ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $report-id as string? := $report
let $report as object? := reports:reports($report-id)

let $report-not-found as boolean := empty($report)

(: Fact resolution :)
let $filter-override as object? :=
    if($profile-name eq "sec")
    then fiscal-core:filter-override(
        $entities,
        $fiscalYear,
        $fiscalPeriod,
        $fiscalPeriodType,
        $aid)
    else components:filter-override($entities, $aid)

let $hypercube := hypercubes:hypercubes-for-components($report)[1]

let $facts as object* :=
    let $filtered-aspects := values($hypercube.Aspects)[exists(($$.Members, $$.DomainRestriction))]
    return if(count($filtered-aspects) lt $config:filtered-aspects and not exists(($filter-override)))
    then {
          { status: 403 },
          session:error("The report filters are too weak, which leads to too big an output.", $format)
    } else if (exists($report))
    then components:facts(
                $report,
                {|
                    { FilterOverride: $filter-override }[exists($filter-override)],
                    { Validate: $validate }
                |}
            )
    else ()

let $concepts as object* := $report.Concepts[]
let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
let $facts :=
  if($profile-name eq "sec")
  then
    for $fact in $facts
    group by $archive := $fact.Aspects."xbrl28:Archive"
    let $archive := archives:archives($archive)
    let $entity := entities:entities($archive.Entity)
    for $fact in $fact
    return
    {|
        trim($fact, ("Labels", "EntityRegistrantName")),
        { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
        if($labels)
        then
            let $labels as object? := labels:labels-for-facts(
              $fact,
              $labels:STANDARD_LABEL_ROLE,
              $language,
              $concepts,
              $entities,
              ()
            )
            return
                { Labels : $labels }
        else ()
    |}
  else
    for $fact in $facts
    let $labels := labels:labels-for-facts(
      $fact,
      $labels:STANDARD_LABEL_ROLE,
      $language,
      $concepts,
      $entities,
      ()
    )
    return
    {|
        trim($fact, "Labels"),
        { Labels : $labels }[exists($labels)]
    |}

let $facts :=
  if($profile-name eq "sec")
  then api:normalize-facts($facts)
  else $facts

let $result :=
  if($profile-name eq "sec")
  then {
     NetworkIdentifier : $report.Role,
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label,
     FactTable : [ $facts ]
  }
  else {
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label,
     FactTable : [ $facts ]
  }

let $comment := {
    NumFacts : count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}

let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
          TableName="xbrl:FactTableForReport">{
            conversion:facts-to-xml($facts, { Caller: "Report" })
        }</FactTable>
        default return <FactTable
            tableName="{$res.TableName}"
            label="{$res.Label}"
            archive="{$res.Archive}"
            role="{$res.Role}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Report" })
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        string-join(conversion:facts-to-csv($res.FactTable[], { Caller: "Report"}))
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $report-not-found return api:not-found("report")
       default return api:check-and-return-results($token, $results, $format)

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $validate      as boolean external := false;
declare  %rest:case-insensitive                 variable $labels        as boolean external := false;
declare  %rest:case-insensitive                 variable $report        as string? external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;
declare  %rest:case-insensitive                 variable $language           as string  external := "en-US";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $fiscalPeriodType as string* := api:preprocess-fiscal-period-types($fiscalPeriodType)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic, ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $report-id as string? := $report
let $report as object? := reports:reports($report-id)

return
if(empty($report))
then
{
      { status: 404 },
      session:error("report with id '" || $report-id || "' does not exist.", $format)
} else

    (: Fact resolution :)
    let $filter-override as object? :=
        if($profile-name eq "sec")
        then fiscal-core:filter-override(
            $entities,
            $fiscalYear,
            $fiscalPeriod,
            $fiscalPeriodType,
            $aid)
        else components:filter-override($entities, $aid)
    let $facts as object* :=
        let $hypercube := hypercubes:hypercubes-for-components($report)[1]
        let $filtered-aspects := values($hypercube.Aspects)[exists(($$.Members, $$.DomainRestriction))]
        return if(count($filtered-aspects) lt $config:filtered-aspects and not exists(($filter-override)))
        then {
              { status: 403 },
              session:error("The report filters are too weak, which leads to too big an output.", $format)
        } else
            components:facts(
                    $report,
                    {|
                        { FilterOverride: $filter-override }[exists($filter-override)],
                        { Validate: $validate }
                    |}
                )

    let $concepts as object* := $report.Concepts[]
    let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
    let $facts :=
      if($profile-name eq "sec")
      then
        for $fact in $facts
        group by $archive := $fact.Aspects."xbrl28:Archive"
        let $archive := archives:archives($archive)
        let $entity := entities:entities($archive.Entity)
        for $fact in $fact
        return
        {|
            trim($fact, ("Labels", "EntityRegistrantName")),
            { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
            if($labels)
            then
                let $labels as object? := labels:labels-for-facts(
                  $fact,
                  $labels:STANDARD_LABEL_ROLE,
                  $language,
                  $concepts,
                  $entities,
                  ()
                )
                return
                    { Labels : $labels }
            else ()
        |}
      else
        for $fact in $facts
        let $labels := labels:labels-for-facts(
          $fact,
          $labels:STANDARD_LABEL_ROLE,
          $language,
          $concepts,
          $entities,
          ()
        )
        return
        {|
            trim($fact, "Labels"),
            { Labels : $labels }[exists($labels)]
        |}

    let $facts := api:normalize-facts($facts)

    let $results :=
        switch ($format)
        case "xml" return {
            {
                serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
            },
            session:comment("xml",
            {
                NumFacts : count($facts),
                TotalNumFacts: session:num-facts(),
                TotalNumArchives: session:num-archives(),
                TotalNumEntities: session:num-entities()
            }),
            <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
                    TableName="xbrl:FactTableForReport">{
                conversion:facts-to-xml($facts, { Caller: "Report" })
            }</FactTable>
        }
        case "text" case "csv" return {
            {
                "content-type": "text/csv",
                headers: { "Content-Disposition": "attachment; filename=facts.csv" }
            },
            conversion:facts-to-csv($facts, { Caller: "Report"})
        }
        case "excel" return {
            {
                "content-type": "application/vnd.ms-excel",
                headers: { "Content-Disposition": "attachment; filename=facts.csv" }
            },
            conversion:facts-to-csv($facts, { Caller: "Report"})
        }
        default return {
            { serialization: { method: "json", indent : true } },
            {|
                { NetworkIdentifier : "http://secxbrl.info/facts" },
                { TableName : "xbrl:Facts" },
                { FactTable : [ $facts ] },
                session:comment("json", {
                        NumFacts : count($facts),
                        TotalNumFacts: session:num-facts(),
                        TotalNumArchives: session:num-archives(),
                        TotalNumEntities: session:num-entities()
                    })
            |}
        }
    return if($entities-not-found)
           then api:not-found("entity")
           else api:check-and-return-results($token, $results, $format)