1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Ext\Cite;
5
6use DOMElement;
7use stdClass;
8use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
9
10class ReferencesData {
11
12	/**
13	 * @var int
14	 */
15	private $index;
16
17	/**
18	 * @var RefGroup[]
19	 */
20	private $refGroups;
21
22	/**
23	 * ReferencesData constructor.
24	 */
25	public function __construct() {
26		$this->index = 0;
27		$this->refGroups = [];
28	}
29
30	/**
31	 * @param string $groupName
32	 * @param bool $allocIfMissing
33	 * @return RefGroup|null
34	 */
35	public function getRefGroup( string $groupName = '', bool $allocIfMissing = false ): ?RefGroup {
36		if ( !isset( $this->refGroups[$groupName] ) && $allocIfMissing ) {
37			$this->refGroups[$groupName] = new RefGroup( $groupName );
38		}
39		return $this->refGroups[$groupName] ?? null;
40	}
41
42	/**
43	 * @param string|null $groupName
44	 */
45	public function removeRefGroup( ?string $groupName = null ): void {
46		if ( $groupName !== null ) {
47			// '' is a valid group (the default group)
48			unset( $this->refGroups[$groupName] );
49		}
50	}
51
52	/**
53	 * @param ParsoidExtensionAPI $extApi
54	 * @param string $groupName
55	 * @param string $refName
56	 * @param string $about
57	 * @param bool $skipLinkback
58	 * @param DOMElement $linkBack
59	 * @return stdClass
60	 */
61	public function add(
62		ParsoidExtensionAPI $extApi, string $groupName, string $refName,
63		string $about, bool $skipLinkback, DOMElement $linkBack
64	): stdClass {
65		$group = $this->getRefGroup( $groupName, true );
66		// Looks like Cite.php doesn't try to fix ids that already have
67		// a "_" in them. Ex: name="a b" and name="a_b" are considered
68		// identical. Not sure if this is a feature or a bug.
69		// It also considers entities equal to their encoding
70		// (i.e. '&' === '&amp;'), which is done:
71		//  in PHP: Sanitizer#decodeTagAttributes and
72		//  in Parsoid: ExtensionHandler#normalizeExtOptions
73		$refName = $extApi->sanitizeHTMLId( $refName );
74		$hasRefName = strlen( $refName ) > 0;
75
76		if ( $hasRefName && isset( $group->indexByName[$refName] ) ) {
77			$ref = $group->indexByName[$refName];
78			if ( $ref->contentId && !$ref->hasMultiples ) {
79				$ref->hasMultiples = true;
80				// Use the non-pp version here since we've already stored attribs
81				// before putting them in the map.
82				$ref->cachedHtml = $extApi->getContentHTML( $ref->contentId );
83			}
84			$ref->nodes[] = $linkBack;
85		} else {
86			// The ids produced Cite.php have some particulars:
87			// Simple refs get 'cite_ref-' + index
88			// Refs with names get 'cite_ref-' + name + '_' + index + (backlink num || 0)
89			// Notes (references) whose ref doesn't have a name are 'cite_note-' + index
90			// Notes whose ref has a name are 'cite_note-' + name + '-' + index
91			$n = $this->index;
92			$refKey = strval( 1 + $n );
93			$refIdBase = 'cite_ref-' . ( $hasRefName ? $refName . '_' . $refKey : $refKey );
94			$noteId = 'cite_note-' . ( $hasRefName ? $refName . '-' . $refKey : $refKey );
95
96			// bump index
97			$this->index += 1;
98
99			$ref = (object)[
100				'about' => $about,
101				'contentId' => null,
102				'dir' => '',
103				'group' => $group->name,
104				'groupIndex' => count( $group->refs ) + 1,
105				'index' => $n,
106				'key' => $refIdBase,
107				'id' => $hasRefName ? $refIdBase . '-0' : $refIdBase,
108				'linkbacks' => [],
109				'name' => $refName,
110				'target' => $noteId,
111				'hasMultiples' => false,
112				// Just used for comparison when we have multiples
113				'cachedHtml' => '',
114				'nodes' => [],
115			];
116			$group->refs[] = $ref;
117			if ( $hasRefName ) {
118				$group->indexByName[$refName] = $ref;
119				$ref->nodes[] = $linkBack;
120			}
121		}
122
123		if ( !$skipLinkback ) {
124			$ref->linkbacks[] = $ref->key . '-' . count( $ref->linkbacks );
125		}
126
127		return $ref;
128	}
129
130	/**
131	 * @return RefGroup[]
132	 */
133	public function getRefGroups(): array {
134		return $this->refGroups;
135	}
136}
137