File: MultipleChoice.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (155 lines) | stat: -rw-r--r-- 8,554 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
 Licensed under Apache License v2.0 with Runtime Library Exception

 See https://swift.org/LICENSE.txt for license information
 See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import Markdown

/// A multiple-choice question.
///
/// A collection of multiple-choice questions that form an ``Assessments``.
public final class MultipleChoice: Semantic, DirectiveConvertible {
    public static let introducedVersion = "5.5"
    public static let directiveName = "MultipleChoice"
    
    /// The phrasing of the question.
    public let questionPhrasing: MarkupContainer
    
    public let originalMarkup: BlockDirective
    
    /// Additional introductory content.
    ///
    /// Typically, this content represents a question's code block.
    public let content: MarkupContainer
    
    /// An optional image associated with the question's introduction.
    public let image: ImageMedia?
    
    /// The possible answers to the question.
    public let choices: [Choice]
    
    override var children: [Semantic] {
        var elements: [Semantic] = [content]
        if let image {
            elements.append(image)
        }
        elements.append(contentsOf: choices)
        return elements
    }
    
    init(originalMarkup: BlockDirective, questionPhrasing: MarkupContainer, content: MarkupContainer, image: ImageMedia?, choices: [Choice]) {
        self.originalMarkup = originalMarkup
        self.questionPhrasing = questionPhrasing
        self.content = content
        self.image = image
        self.choices = choices
    }
    
    public convenience init?(from directive: BlockDirective, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) {
        precondition(directive.name == MultipleChoice.directiveName)
        
        _ = Semantic.Analyses.HasOnlyKnownArguments<MultipleChoice>(severityIfFound: .warning, allowedArguments: []).analyze(directive, children: directive.children, source: source, for: bundle, in: context, problems: &problems)
        
        Semantic.Analyses.HasOnlyKnownDirectives<MultipleChoice>(severityIfFound: .warning, allowedDirectives: [Choice.directiveName, ImageMedia.directiveName]).analyze(directive, children: directive.children, source: source, for: bundle, in: context, problems: &problems)
        
        var remainder = MarkupContainer(directive.children)
        let requiredPhrasing: Paragraph?
        if let paragraph = remainder.first as? Paragraph {
            requiredPhrasing = paragraph
            remainder = MarkupContainer(remainder.elements.suffix(from: 1))
        } else {
            let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).missingPhrasing", summary: " \(MultipleChoice.directiveName.singleQuoted) directive is missing its initial paragraph that serves as a question's title phrasing")
            problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
            requiredPhrasing = nil
        }
        
        let choices: [Choice]
        (choices, remainder) = Semantic.Analyses.ExtractAll<Choice>().analyze(directive, children: remainder, source: source, for: bundle, in: context, problems: &problems)
        
        if choices.count < 2 || choices.count > 4 {
            let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).CorrectNumberOfChoices", summary: "`\(MultipleChoice.directiveName)` should contain 2-4 `\(Choice.directiveName)` child directives")
            problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
        }
        
        let correctAnswers = choices.filter({ $0.isCorrect })
        
        if correctAnswers.isEmpty {
            let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).CorrectChoiceProvided", summary: "`\(MultipleChoice.directiveName)` should contain `\(Choice.directiveName)` directive marked as the correct option")
            problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
        } else if correctAnswers.count > 1 {
            var diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).MultipleCorrectChoicesProvided", summary: "`\(MultipleChoice.directiveName)` should contain exactly one `\(Choice.directiveName)` directive marked as the correct option")
            for answer in correctAnswers {
                guard let range = answer.originalMarkup.range else {
                    continue
                }
                if let source {
                    let note = DiagnosticNote(source: source, range: range, message: "This `\(Choice.directiveName)` directive is marked as the correct option")
                    diagnostic.notes.append(note)
                }
            }
            problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
        }
        
        let codeBlocks = remainder.compactMap { $0 as? CodeBlock }
        
        func removeExtraneous(_ elementName: String, range: SourceRange) -> Solution {
            return Solution(summary: "Remove extraneous code", replacements: [
                Replacement(range: range, replacement: "")
            ])
        }
        
        if codeBlocks.count > 1 {
            for extraneousCode in codeBlocks.suffix(from: 1) {
                guard let range = extraneousCode.range else {
                    continue
                }
                let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).ExtraneousCode", summary: "`\(MultipleChoice.directiveName)` may contain only one markup code block following the question's phrasing.")
                problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [removeExtraneous("code block", range: range)]))
            }
        }
        
        let images: [ImageMedia]
        (images, remainder) = Semantic.Analyses.ExtractAll<ImageMedia>().analyze(directive, children: remainder, source: source, for: bundle, in: context, problems: &problems)
        
        if images.count > 1 {
            for extraneousImage in images.suffix(from: 1) {
                guard let range = extraneousImage.originalMarkup.range else {
                    continue
                }
                let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).ExtraneousImage", summary: "`\(MultipleChoice.directiveName)` may contain only one '\(ImageMedia.directiveName)' directive following the question's phrasing")
                problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [removeExtraneous(ImageMedia.directiveName, range: range)]))
            }
        }
        
        if codeBlocks.count == 1 && images.count == 1 {
            let codeBlock = codeBlocks.first!
            let image = images.first!
            let diagnostic = Diagnostic(source: source, severity: .warning, range: directive.range, identifier: "org.swift.docc.\(MultipleChoice.self).CodeOrImage", summary: "`\(MultipleChoice.directiveName)` may contain an `\(ImageMedia.directiveName)` or markup code block following the question's phrasing")
            let solutions = [
                codeBlock.range.map { range -> Solution in
                    removeExtraneous("code", range: range)
                },
                image.originalMarkup.range.map { range -> Solution in
                    removeExtraneous(ImageMedia.directiveName, range: range)
                },
            ].compactMap { $0 }
            problems.append(Problem(diagnostic: diagnostic, possibleSolutions: solutions))
        }
        
        guard let questionPhrasing = requiredPhrasing else {
            return nil
        }

        self.init(originalMarkup: directive, questionPhrasing: MarkupContainer(questionPhrasing), content: MarkupContainer(remainder), image: images.first, choices: choices)
    }
    
    public override func accept<V: SemanticVisitor>(_ visitor: inout V) -> V.Result {
        return visitor.visitMultipleChoice(self)
    }
}